publishport-opencli 1.0.30 → 1.0.32
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 +345 -71
- package/clis/_shared/article/login.js +1 -1
- package/clis/_shared/login-wait.js +1 -0
- package/clis/_shared/site-auth.js +1 -1
- package/clis/baijiahao/article.js +2 -2
- package/clis/baijiahao/auth.js +18 -0
- package/clis/baijiahao/lists.js +1 -1
- package/clis/baijiahao/stats.js +1 -1
- package/clis/bilibili/coin.js +1 -1
- package/clis/bilibili/comment.js +1 -1
- package/clis/bilibili/comments.js +1 -1
- package/clis/bilibili/dm-list.js +1 -1
- package/clis/bilibili/dm-read.js +1 -1
- package/clis/bilibili/favorite-add.js +1 -1
- package/clis/bilibili/me.js +1 -1
- package/clis/bilibili/search.js +1 -1
- package/clis/bilibili/stats.js +1 -1
- package/clis/bilibili/user-videos.js +1 -1
- package/clis/bilibili/utils.js +7 -7
- package/clis/cnblogs/article.js +1 -1
- package/clis/csdn/stats.js +7 -13
- package/clis/devto/publish.js +20 -4
- package/clis/douyin/_shared/work-list.js +1 -0
- package/clis/douyin/publish.js +9 -8
- package/clis/douyin/stats.js +1 -1
- package/clis/douyin/videos.js +1 -1
- package/clis/jike/comments.js +2 -1
- package/clis/jike/like.js +1 -1
- package/clis/jike/notifications.js +2 -115
- package/clis/jike/post.js +2 -2
- package/clis/jike/topic.js +1 -1
- package/clis/jike/topics.js +3 -0
- package/clis/nowcoder/auth.js +1 -24
- package/clis/nowcoder/comment.js +1 -1
- package/clis/nowcoder/comments.js +1 -1
- package/clis/nowcoder/delete.js +1 -1
- package/clis/nowcoder/detail.js +1 -48
- package/clis/nowcoder/dm-list.js +1 -1
- package/clis/nowcoder/dm-read.js +1 -1
- package/clis/nowcoder/dm-send.js +1 -1
- package/clis/nowcoder/experience.js +1 -22
- package/clis/nowcoder/inbox.js +1 -1
- package/clis/nowcoder/like-status.js +1 -0
- package/clis/nowcoder/like.js +1 -1
- package/clis/nowcoder/my.js +1 -1
- package/clis/nowcoder/notifications.js +1 -16
- package/clis/nowcoder/post.js +1 -1
- package/clis/nowcoder/recommend.js +1 -1
- package/clis/nowcoder/search.js +1 -32
- package/clis/nowcoder/utils.js +38 -44
- package/clis/qiita/publish.js +2 -2
- package/clis/quora/__fixtures__/dom.html +24 -0
- package/clis/quora/answer-detail.js +1 -1
- package/clis/quora/question.js +25 -1
- package/clis/quora/search.js +1 -1
- package/clis/quora/shared.js +18 -17
- package/clis/quora/upvote.js +5 -13
- package/clis/reddit/comment.js +25 -25
- package/clis/reddit/reply.js +7 -5
- package/clis/reddit/search.js +1 -51
- package/clis/reddit/shared.js +13 -0
- package/clis/reddit/subreddit-info.js +10 -4
- package/clis/reddit/subreddit-rules.js +1 -0
- package/clis/reddit/subreddit.js +1 -52
- package/clis/reddit/upvote.js +33 -10
- package/clis/reddit/user-comments.js +1 -20
- package/clis/reddit/user-posts.js +1 -17
- package/clis/twitter/post.js +63 -39
- package/clis/wechat-channels/auth.js +34 -31
- package/clis/wechat-channels/publish.js +295 -130
- package/clis/xianyu/watch.js +43 -1
- package/clis/xiaohongshu/_shared/note-manager.js +37 -0
- package/clis/xiaohongshu/creator-notes.js +9 -43
- package/clis/xiaohongshu/delete-note.js +7 -19
- package/clis/xiaohongshu/draft-delete.js +2 -2
- package/clis/xiaohongshu/publish.js +34 -43
- package/clis/xiaohongshu/search.js +18 -3
- package/clis/youtube/publish.js +11 -11
- package/clis/zhihu/answer-comments.js +2 -11
- package/clis/zhihu/answer-detail.js +1 -11
- package/clis/zhihu/comment.js +4 -4
- package/clis/zhihu/like.js +17 -3
- package/clis/zhihu/question.js +20 -6
- package/clis/zhihu/read-shared.js +26 -0
- package/clis/zhihu/recommend.js +1 -7
- package/clis/zhihu/search.js +1 -11
- package/dist/src/browser/base-page.d.ts +41 -1
- package/dist/src/browser/base-page.js +29 -16
- package/dist/src/browser/find.d.ts +31 -4
- package/dist/src/browser/find.js +76 -23
- package/dist/src/browser/utils.d.ts +30 -0
- package/dist/src/browser/utils.js +7 -1
- package/dist/src/cli.js +22 -22
- package/dist/src/commanderAdapter.d.ts +14 -0
- package/dist/src/commanderAdapter.js +4 -3
- package/dist/src/doctor.d.ts +7 -0
- package/dist/src/doctor.js +9 -9
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.d.ts +1 -0
- package/dist/src/execution.js +2 -2
- package/dist/src/main.js +6 -6
- package/dist/src/types.d.ts +28 -0
- package/package.json +1 -1
package/clis/nowcoder/inbox.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as p,Strategy as y}from"@jackwener/opencli/registry";import{ArgumentError as _,CommandExecutionError as m,EmptyResultError as a}from"@jackwener/opencli/errors";import{GW_BASE as g,getJson as l,getSession as f,noteTrustedEmpty as w,plainText as d,requireOk as h,toIso as b}from"./utils.js";function E(t){const i=t.sourceEntity??{},n=t.toEntity??{},r=String(t.pcUrl??""),e=Number(i.entityType),o=e===74?String(i.entityId??""):r.match(/\/discuss\/(\d+)/)?.[1]??"",c=Number(n.entityType)===2;return{msg_id:t.id??"",from_user_id:t.user?.userId??"",from_nickname:t.user?.nickname??"",reply_content:d(t.triggerEntity?.content),reply_comment_id:t.triggerEntity?.entityId??"",our_comment_id:c?String(n.entityId??""):"",our_comment:c?d(n.content):"",post_id:o,post_entity_type:e===74?74:o?250:Number.isFinite(e)?e:"",post_title:d(i.content),post_quote:String(t.quote?.content??"").trim(),url:r,time:b(t.createTime)}}p({site:"nowcoder",name:"inbox",access:"read",description:"评论回复收件箱(谁回复/@了我们,含定向回复所需的评论 id)",domain:"www.nowcoder.com",strategy:y.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["msg_id","from_user_id","from_nickname","reply_content","reply_comment_id","our_comment_id","our_comment","post_id","post_entity_type","post_title","post_quote","url","time"],func:async(t,i)=>{const n=i.page;if(!Number.isSafeInteger(n)||n<1)throw new _("--page 必须是正整数");let r,e;for(let o=1;o<=n;o++){const c=r===void 0?"":`?startIndex=${encodeURIComponent(r)}`,u=h(await l(t,`${g}/message/pc/comment/list${c}`),"评论收件箱");e=u.data?.records;if(!Array.isArray(e))throw new m("牛客评论收件箱未返回 records 数组");if(o===n)break;if(e.length===0||u.data.hasMore===!1)throw new a("nowcoder inbox",`收件箱在第 ${o} 页就结束了,第 ${n} 页不存在。接口正常返回,这是可信的「到底了」,不是读取失败。`);const s=e[e.length-1].id;if(!(typeof s==="string"&&s.length>0||Number.isSafeInteger(s)))throw new m("牛客评论收件箱未返回精确的分页游标");if(String(s)===r)throw new m("牛客评论收件箱分页游标没有前进");r=String(s)}if(e.length===0){const{uid:o}=await f(t);if(n>1)throw new a("nowcoder inbox",`第 ${n} 页已超出范围(当前账号 uid=${o} 的收件箱在更前面的页就结束了)。`);return w("inbox",`已鉴权(当前账号 uid=${o}),服务端返回 0 条评论/回复消息。`)}return e.map(E)}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as r,Strategy as d}from"@jackwener/opencli/registry";import{getSession as s,readLikeState as n}from"./utils.js";r({site:"nowcoder",name:"like-status",access:"read",description:"查某帖当前账号是否已赞(只读)",domain:"www.nowcoder.com",strategy:d.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(18 位讨论帖 id / 动态 uuid / 帖子链接)"}],columns:["post_id","liked","like_count","uid","url"],func:async(e,t)=>{const o=await s(e),i=await n(e,t.id);return[{post_id:i.id,liked:i.liked,like_count:i.likeCount??"",uid:o.uid,url:i.url}]}});
|
package/clis/nowcoder/like.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as r,Strategy as a}from"@jackwener/opencli/registry";import{WWW_BASE as c,getSession as k,postMultipart as m,requireOk as f,resolvePostTarget as s}from"./utils.js";r({site:"nowcoder",name:"like",access:"write",description:"点赞帖子(幂等:已赞不重复操作;--undo 取消赞;有频控,务必低频)",domain:"www.nowcoder.com",strategy:a.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(18 位讨论帖 id / 动态 uuid / 帖子链接 / 7 位 momentId;momentId 读不到当前状态,不判重也不回读)"},{name:"undo",type:"boolean",default:!1,help:"取消赞(默认是确保已赞)"}],columns:["post_id","status","liked","confirmed","like_count","uid","url","note"],func:async(t,d)=>{const e=!d.undo,n=await k(t),i=await s(t,d.id),u=i.liked!==null;if(u&&i.liked===e)return[{post_id:i.id,status:e?"already_liked":"already_unliked",liked:e,confirmed:!0,like_count:i.likeCount??"",uid:n.uid,url:i.url,note:""}];const l=f(await m(t,`${c}${e?"/sns/like":"/sns/del_like"}?_=${Date.now()}`,{id:i.id,type:String(i.entityType)}),e?"点赞":"取消赞",{id:d.id});if(!u)return[{post_id:i.id,status:e?"liked":"unliked",liked:e,confirmed:!1,like_count:l.count??"",uid:n.uid,url:null,note:"未能回读状态:7 位 momentId 查不到点赞状态,写前未判重、写后未核对。"+"要幂等和回读确认请改传该动态的 uuid 或 /feed/main/detail/<uuid> 链接。"}];const o=await s(t,d.id);return[{post_id:i.id,status:e?"liked":"unliked",liked:e,confirmed:o.liked===e,like_count:l.count??o.likeCount??"",uid:n.uid,url:i.url,note:o.liked===e?"":"接口返回成功,但回读到的状态与预期不一致(可能是读缓存延迟);稍后用 like-status 核对,不要立刻重试。"}]}});
|
package/clis/nowcoder/my.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as s,Strategy as d}from"@jackwener/opencli/registry";import{ArgumentError as m,EmptyResultError as u}from"@jackwener/opencli/errors";import{GW_BASE as l,feedRecordToPost as p,getJson as y,getSession as g,noteTrustedEmpty as w,requireOk as f}from"./utils.js";s({site:"nowcoder",name:"my",access:"read",description:"我发布的内容列表(动态 + 长文,含可供 delete 使用的 id)",domain:"www.nowcoder.com",strategy:d.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["content_id","kind","title","content","url","views","likes","comments","created_at","uuid","entity_type","uid","total"],func:async(i,e)=>{if(!Number.isSafeInteger(e.page)||e.page<1)throw new m("--page 必须是正整数");const{uid:n}=await g(i),c=`${l}/user/content/my-content?pageNo=${e.page}&userId=${n}&_=${Date.now()}`,o=f(await y(i,c),"我的内容").data??{},r=o.records??[];if(r.length===0){if(e.page>1)throw new u("nowcoder my",`第 ${e.page} 页已超出范围(当前账号 uid=${n} 的内容在更前面的页就结束了)。`);return w("my",`已鉴权(当前账号 uid=${n}),服务端 total=${Number(o.total??0)},该账号还没有发布过内容。`)}return r.map((a)=>{const t=p(a);return{content_id:t.writeId,kind:t.entityType===74?"moment":a.contentData?.typeName??String(t.entityType??""),title:t.title,content:t.content.replace(/\s+/g," ").slice(0,120),url:t.url,views:t.views??0,likes:t.likes??0,comments:t.comments??0,created_at:t.time,uuid:t.uuid,entity_type:t.entityType,uid:n,total:Number(o.total??r.length)}})}});
|
|
@@ -1,16 +1 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/message/pc/unread/detail', {credentials: 'include'});
|
|
3
|
-
const d = await r.json();
|
|
4
|
-
if (!d.success) throw new Error(d.msg || 'API failed');
|
|
5
|
-
const data = d.data;
|
|
6
|
-
return [
|
|
7
|
-
{type: 'system', unread: data.systemNotice?.unreadCount || 0},
|
|
8
|
-
{type: 'likes', unread: data.likeCollect?.unreadCount || 0},
|
|
9
|
-
{type: 'comments', unread: data.commentMessage?.unreadCount || 0},
|
|
10
|
-
{type: 'follows', unread: data.followMessage?.unreadCount || 0},
|
|
11
|
-
{type: 'messages', unread: data.privateMessage?.unreadCount || 0},
|
|
12
|
-
{type: 'job_apply', unread: data.nowPickJobApply?.unreadCount || 0},
|
|
13
|
-
{type: 'total', unread: data.total?.unreadCount || 0},
|
|
14
|
-
];
|
|
15
|
-
})()
|
|
16
|
-
`}]});
|
|
1
|
+
import{cli as n,Strategy as r}from"@jackwener/opencli/registry";import{GW_BASE as i,getJson as u,getSession as c,requireOk as d}from"./utils.js";const l=[["system","systemNotice"],["likes","likeCollect"],["flowers","flowerPersonal"],["comments","commentMessage"],["follows","followMessage"],["messages","privateMessage"],["job_apply","nowPickJobApply"],["job_interact","nowPickInteract"],["total","total"]],m=[["nav_notify","unreadNotifyCount"],["nav_private_messages","unreadPrivateMsgCount"],["nav_total","unreadMsgCount"]];n({site:"nowcoder",name:"notifications",access:"read",description:"未读消息汇总(分类未读 + 导航栏红点总数,后者含系统通知)",domain:"www.nowcoder.com",strategy:r.COOKIE,args:[],columns:["type","unread","source","uid"],func:async(a)=>{const e=await c(a),s=d(await u(a,`${i}/message/pc/unread/detail`),"未读明细").data??{};return[...l.map(([o,t])=>({type:o,unread:Number(s[t]?.unreadCount??0),source:"unread/detail",uid:e.uid})),...m.map(([o,t])=>({type:o,unread:Number(e.unread[t]??0),source:"unread-count-v2",uid:e.uid}))]}});
|
package/clis/nowcoder/post.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as s,Strategy as m}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as c}from"@jackwener/opencli/errors";import{GW_BASE as d,XHR_HEADERS as l,getSession as u,postJson as p,requireOk as w}from"./utils.js";import{assertLiteralContent as f}from"../_shared/content-guard.js";s({site:"nowcoder",name:"post",access:"write",description:"发布动态(实名发帖,高风险写操作,务必低频且内容有价值)",domain:"www.nowcoder.com",strategy:m.COOKIE,args:[{name:"content",fromFile:!0,positional:!0,required:!0,help:"正文内容"},{name:"title",type:"str",default:"",help:"标题(可选)"}],columns:["moment_id","content_id","url","uid"],func:async(n,o)=>{const e=String(o.content??"").trim();f(e,{label:"正文内容"});if(!e)throw new a("发帖正文不能为空");const i=await u(n),r=w(await p(n,`${d}/moment/create`,{title:String(o.title??""),content:e,type:0,isAnonymousFlag:!1,publicEntrance:"发动态",publicEntrancePage:"首页"},l),"发帖",{dumpBody:!0}),t=r.data??{};if(!t.id)throw new c(`牛客发帖响应缺少 momentId,完整响应: ${JSON.stringify(r).slice(0,2000)}`);return[{moment_id:t.id,content_id:t.result??"",url:t.result?`https://www.nowcoder.com/feed/main/detail/${t.result}`:"",uid:i.uid}]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as l,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as d,CommandExecutionError as f}from"@jackwener/opencli/errors";import{GW_BASE as y,feedRecordToPost as g,noteTrustedEmpty as c,requireOk as h}from"./utils.js";l({site:"nowcoder",name:"recommend",access:"read",description:"Recommended feed",domain:"www.nowcoder.com",strategy:w.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","uuid","userId","entityType","url","time"],func:async(u)=>{const{page:t,limit:r}=u;if(!Number.isSafeInteger(t)||t<1)throw new d("--page 必须是正整数");if(!Number.isSafeInteger(r)||r<1)throw new d("--limit 必须是正整数");const m=await fetch(`${y}/home/recommend?page=${t}&size=${r}`),a=await m.text();let n;try{n=JSON.parse(a)}catch{n={__transport:{status:m.status,preview:a.slice(0,200)}}}const i=h(n,"推荐流").data?.records;if(!Array.isArray(i))throw new f("牛客推荐流未返回 records 数组");if(i.length===0)return c("recommend",`推荐流第 ${t} 页服务端返回 0 条记录。`);const o=i.map(g).filter((e)=>e.writeId);if(o.length===0)return c("recommend",`推荐流第 ${t} 页服务端返回 ${i.length} 条记录,全是非帖子卡片、没有帖子。`);return o.slice(0,r).map((e,s)=>{return{rank:s+1,title:e.title,author:e.author,likes:e.likes,comments:e.comments,views:e.views,content:e.content,id:e.entityType===74?e.uuid:e.writeId,momentId:e.writeId,uuid:e.uuid,userId:e.userId,entityType:e.entityType,url:e.url,time:e.time}})}});
|
package/clis/nowcoder/search.js
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
const query = \${{ args.query | json }};
|
|
3
|
-
const type = \${{ args.type | json }};
|
|
4
|
-
const limit = \${{ args.limit }};
|
|
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();
|
|
7
|
-
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/pc/search', {
|
|
8
|
-
method: 'POST',
|
|
9
|
-
credentials: 'include',
|
|
10
|
-
headers: {'Content-Type': 'application/json'},
|
|
11
|
-
body: JSON.stringify({query, type, page: 1, pageSize: limit})
|
|
12
|
-
});
|
|
13
|
-
const d = await r.json();
|
|
14
|
-
if (!d.success) throw new Error(d.msg || 'search failed');
|
|
15
|
-
return (d.data?.records || []).map((item, i) => {
|
|
16
|
-
const data = item.data || {};
|
|
17
|
-
const moment = data.momentData || {};
|
|
18
|
-
const contentData = data.contentData || {};
|
|
19
|
-
const user = data.userBrief || {};
|
|
20
|
-
const uuid = moment.uuid || contentData.uuid || '';
|
|
21
|
-
const id = data.contentId || '';
|
|
22
|
-
return {
|
|
23
|
-
rank: i + 1,
|
|
24
|
-
title: strip(moment.title || contentData.title || user.nickname || ''),
|
|
25
|
-
author: user.nickname || '',
|
|
26
|
-
school: user.educationInfo || '',
|
|
27
|
-
content: strip(moment.content || contentData.content || ''),
|
|
28
|
-
id: uuid || id,
|
|
29
|
-
};
|
|
30
|
-
}).filter(r => r.title);
|
|
31
|
-
})()
|
|
32
|
-
`},{limit:"${{ args.limit }}"}]});
|
|
1
|
+
import{cli as u,Strategy as c}from"@jackwener/opencli/registry";import{ArgumentError as l,CommandExecutionError as d}from"@jackwener/opencli/errors";import{GW_BASE as p,feedRecordToPost as y,noteTrustedEmpty as h,postJson as f,requireOk as w}from"./utils.js";u({site:"nowcoder",name:"search",access:"read",description:"Full-text search",domain:"www.nowcoder.com",strategy:c.COOKIE,args:[{name:"query",positional:!0,required:!0,help:"Search keyword"},{name:"type",type:"str",default:"all",help:"Search type (all/post/question/user/job)"},{name:"limit",type:"int",default:10,help:"Number of results"}],columns:["rank","title","author","school","content","id","momentId","uuid","entityType","url","likes","comments","time"],func:async(m,o)=>{const i=String(o.query??"").trim();if(!i)throw new l("搜索关键词不能为空");const r=o.limit;if(!Number.isSafeInteger(r)||r<1)throw new l("--limit 必须是正整数");const a=w(await f(m,`${p}/pc/search`,{query:i,type:String(o.type??"all"),page:1,pageSize:r}),"搜索"),n=a.data?.records;if(!Array.isArray(n))throw new d("牛客搜索未返回 records 数组");const s=n.map((e)=>{const t=y(e.data??{});return{title:t.title||t.author,author:t.author,school:t.school,content:t.content,id:t.entityType===74?t.uuid:t.writeId,momentId:t.writeId,uuid:t.uuid,entityType:t.entityType,url:t.url,likes:t.likes,comments:t.comments,time:t.time}}).filter((e)=>e.title).slice(0,r).map((e,t)=>({rank:t+1,...e}));if(s.length===0)return h("search",`「${i}」零命中,服务端 total=${Number(a.data?.total??0)}、返回 ${n.length} 条记录。`);return s}});
|
package/clis/nowcoder/utils.js
CHANGED
|
@@ -1,66 +1,60 @@
|
|
|
1
|
-
import{ArgumentError as
|
|
2
|
-
async () => {
|
|
3
|
-
const res = await fetch(${t}, { credentials: 'include' });
|
|
1
|
+
import{ArgumentError as f,AuthRequiredError as S,CliError as w,CommandExecutionError as d,EXIT_CODES as l}from"@jackwener/opencli/errors";export const GW_BASE="https://gw-c.nowcoder.com/api/sparta",WWW_BASE="https://www.nowcoder.com",XHR_HEADERS={Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"};const p=`
|
|
4
2
|
const text = await res.text();
|
|
5
3
|
try {
|
|
6
4
|
return JSON.parse(text);
|
|
7
5
|
} catch {
|
|
8
|
-
return {
|
|
9
|
-
}
|
|
6
|
+
return { __transport: { status: res.status, preview: text.slice(0, 200) } };
|
|
7
|
+
}`;export async function getJson(t,e){const n=JSON.stringify(e);return t.evaluate(`
|
|
8
|
+
async () => {
|
|
9
|
+
const res = await fetch(${n}, { credentials: 'include' });${p}
|
|
10
10
|
}
|
|
11
|
-
`)}export async function postJson(e,
|
|
11
|
+
`)}export async function postJson(t,e,n,o={}){const i=JSON.stringify(e),r=JSON.stringify(JSON.stringify(n)),s=JSON.stringify({"Content-Type":"application/json",...o});return t.evaluate(`
|
|
12
12
|
async () => {
|
|
13
|
-
const res = await fetch(${
|
|
13
|
+
const res = await fetch(${i}, {
|
|
14
14
|
method: 'POST',
|
|
15
15
|
credentials: 'include',
|
|
16
|
-
headers: ${
|
|
17
|
-
body: ${
|
|
18
|
-
})
|
|
19
|
-
const text = await res.text();
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(text);
|
|
22
|
-
} catch {
|
|
23
|
-
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
24
|
-
}
|
|
16
|
+
headers: ${s},
|
|
17
|
+
body: ${r},
|
|
18
|
+
});${p}
|
|
25
19
|
}
|
|
26
|
-
`)}export async function postForm(e,
|
|
20
|
+
`)}export async function postForm(t,e,n,o={}){const i=JSON.stringify(e),r=JSON.stringify(n),s=JSON.stringify({"Content-Type":"application/x-www-form-urlencoded","x-requested-with":"XMLHttpRequest",...o});return t.evaluate(`
|
|
27
21
|
async () => {
|
|
28
|
-
const body = new URLSearchParams(${
|
|
29
|
-
const res = await fetch(${
|
|
22
|
+
const body = new URLSearchParams(${r});
|
|
23
|
+
const res = await fetch(${i}, {
|
|
30
24
|
method: 'POST',
|
|
31
25
|
credentials: 'include',
|
|
32
|
-
headers: ${
|
|
26
|
+
headers: ${s},
|
|
33
27
|
body: body.toString(),
|
|
34
|
-
})
|
|
35
|
-
const text = await res.text();
|
|
36
|
-
try {
|
|
37
|
-
return JSON.parse(text);
|
|
38
|
-
} catch {
|
|
39
|
-
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
40
|
-
}
|
|
28
|
+
});${p}
|
|
41
29
|
}
|
|
42
|
-
`)}export async function postMultipart(e,
|
|
30
|
+
`)}export async function postMultipart(t,e,n,o={}){const i=JSON.stringify(e),r=JSON.stringify(n),s=JSON.stringify(o);return t.evaluate(`
|
|
43
31
|
async () => {
|
|
44
32
|
const fd = new FormData();
|
|
45
|
-
for (const [k, v] of Object.entries(${
|
|
46
|
-
const res = await fetch(${
|
|
33
|
+
for (const [k, v] of Object.entries(${r})) fd.append(k, v);
|
|
34
|
+
const res = await fetch(${i}, {
|
|
47
35
|
method: 'POST',
|
|
48
36
|
credentials: 'include',
|
|
49
|
-
headers: ${
|
|
37
|
+
headers: ${s},
|
|
50
38
|
body: fd,
|
|
51
|
-
})
|
|
52
|
-
const text = await res.text();
|
|
53
|
-
try {
|
|
54
|
-
return JSON.parse(text);
|
|
55
|
-
} catch {
|
|
56
|
-
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
57
|
-
}
|
|
39
|
+
});${p}
|
|
58
40
|
}
|
|
59
|
-
`)}export
|
|
41
|
+
`)}export class NowcoderError extends w{constructor(t,e,n,o=l.GENERIC_ERROR,i={}){super(t,e,n,o);this.bizCode=i.bizCode;this.bizMsg=i.bizMsg}}const I=new Set([999,998]),T=1125,E=1003,N=2001020001,O=[{test:(t)=>t===T,code:"CAPTCHA_REQUIRED",exit:l.NOPERM,hint:"这不是验证码输错,也不是配额用完:平台要求先过一次滑块人机验证。需要在浏览器里打开该帖页面手动评论一次、"+"过掉滑块后再重试;原样重试接口没有用。"},{test:(t,e)=>/操作太快/.test(e),code:"RATE_LIMITED",exit:l.TEMPFAIL,hint:"频控冷却挂在单个目标上:换一个目标可以继续操作;同一个目标要等十几分钟再试,立刻重试只会延长冷却。"},{test:(t,e)=>/禁言/.test(e),code:"ACCOUNT_MUTED",exit:l.NOPERM,hint:"当前账号被平台禁言,所有写操作都会失败;重试、换目标都没用,需等禁言期结束或换账号。"},{test:(t,e)=>/对方已设置限制/.test(e),code:"TARGET_RESTRICTED",exit:l.NOPERM,hint:"对方关闭了评论/互动权限,这个目标做不了该操作;不要重试,换一个目标。"},{test:(t,e)=>/没有权限/.test(e),code:"NO_PERMISSION",exit:l.NOPERM,hint:"当前账号对该资源没有权限。私信场景多为新号尚未开通私信权限,或读取了不属于自己的会话;重试无用。"},{test:(t,e)=>t===N||/内容不存在/.test(e),code:"NOT_FOUND",exit:l.EMPTY_RESULT,hint:"先核对 id 是否完整、有没有抄错位(讨论帖是 18 位雪花号,动态是 32 位 uuid 或 7 位 momentId);"+"也可能是帖子已删除或仅作者可见。"},{test:(t)=>t===E,code:"SENSITIVE_WORD",exit:l.GENERIC_ERROR,hint:"内容命中平台敏感词被拒,需改写正文后再提交;原样重试无用。"}];function g(t){return t?.id===void 0||t?.id===null||t.id===""?"":` [输入 id: ${t.id}]`}export function requireOk(t,e,n={}){if(t&&typeof t==="object"&&t.__transport){const{status:u,preview:c}=t.__transport;throw new NowcoderError("UPSTREAM_UNAVAILABLE",`牛客 ${e} 返回了非 JSON 响应 (HTTP ${u}): ${String(c??"").replace(/\s+/g," ").slice(0,120)}${g(n)}`,"服务端暂时不可用或被网关拦截,不代表没有数据,也不代表未登录;稍后可以原样重试。",l.TEMPFAIL,{bizCode:u})}if(!t||typeof t!=="object"||Array.isArray(t))throw new d(`牛客 ${e} 接口返回了畸形响应${g(n)}`);if(Object.hasOwn(t,"success")?!!t.success:Object.hasOwn(t,"code")?t.code===0:Object.hasOwn(t,"data"))return t;const i=String(t.msg??t.message??"未知错误"),r=t.code,s=` (code=${r??"?"})${g(n)}`;if(I.has(r)||/need login|请登录|未登录|登陆|未授权/i.test(i))throw new S("nowcoder.com",`牛客 ${e} 需要登录: ${i}${s}`);for(const u of O)if(u.test(r,i))throw new NowcoderError(u.code,`牛客 ${e} 失败: ${i}${s}`,u.hint,u.exit,{bizCode:r,bizMsg:i});const a=n.dumpBody?` 完整响应: ${JSON.stringify(t).slice(0,2000)}`:"";throw new d(`牛客 ${e} 失败: ${i}${s}${a}`)}export async function getSession(t){const e=await t.evaluate(`
|
|
60
42
|
async () => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
43
|
+
const readJson = async (res) => {
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
try { return JSON.parse(text); } catch { return { __transport: { status: res.status, preview: text.slice(0, 200) } }; }
|
|
46
|
+
};
|
|
47
|
+
const [unreadRes, userRes] = await Promise.all([
|
|
48
|
+
fetch(${JSON.stringify(`${WWW_BASE}/nccommon/sns/message/unread-count-v2`)}, { credentials: 'include', cache: 'no-store' }),
|
|
49
|
+
fetch(${JSON.stringify(`${WWW_BASE}/nccommon/profile/user-info-v2`)}, { credentials: 'include', cache: 'no-store' }),
|
|
50
|
+
]);
|
|
51
|
+
const unread = await readJson(unreadRes);
|
|
52
|
+
const user = await readJson(userRes);
|
|
53
|
+
const info = user && user.data && typeof user.data === 'object' ? user.data : null;
|
|
54
|
+
return {
|
|
55
|
+
unread,
|
|
56
|
+
user: info ? { code: user.code, msg: user.msg, data: { userId: info.userId, nickname: info.nickname } } : user,
|
|
57
|
+
};
|
|
65
58
|
}
|
|
66
|
-
`)
|
|
59
|
+
`),n=requireOk(e?.unread,"登录态"),o=requireOk(e?.user,"当前用户").data??{};if(!/^[1-9]\d*$/.test(String(o.userId??"")))throw new d("牛客登录态接口确认已登录,但当前用户接口没有给出 uid","多为接口暂时异常,可稍后重试;不要把这当成未登录。");return{uid:String(o.userId),nickname:String(o.nickname??""),unread:n.data??{}}}export async function getSelfUid(t){return(await getSession(t)).uid}export function noteTrustedEmpty(t,e){process.stderr.write(`nowcoder ${t}: ${e}空列表是真实结果,不是取数失败。
|
|
60
|
+
`);return[]}export function postUrl({entityType:t,id:e,uuid:n}){if(Number(t)===74)return n?`${WWW_BASE}/feed/main/detail/${n}`:null;if(Number(t)===250)return/^\d{15,}$/.test(String(e??""))?`${WWW_BASE}/discuss/${e}`:null;return null}export function toIso(t){const e=Number(t);return Number.isFinite(e)&&e>0?new Date(e).toISOString():""}export function plainText(t){return decodeHtmlEntities(String(t??"").replace(/<[^>]+>/g,"")).trim()}export function feedRecordToPost(t){const e=Number(t?.contentType),n=t?.momentData,o=t?.contentData,i=n??o??{},r=t?.frequencyData??null,s=t?.userBrief??{},a=n?String(n.id??""):String(o?.id??""),u=String(i.uuid??"");return{writeId:a,uuid:u,entityType:Number.isFinite(e)?e:null,url:postUrl({entityType:e,id:a,uuid:u}),title:plainText(i.title??i.newTitle),content:plainText(i.content??i.newContent),author:String(s.nickname??""),userId:String(s.userId??i.userId??i.authorId??""),school:String(s.educationInfo??""),likes:r?Number(r.likeCnt??0):null,comments:r?Number(r.totalCommentCnt??r.commentCnt??0):null,views:r?Number(r.viewCnt??0):null,liked:r&&typeof r.isLike==="boolean"?r.isLike:null,time:toIso(n?n.createdAt:o?.createTime)}}function h(t,e){return new NowcoderError("NOT_VISIBLE",`牛客帖子 ${t} 不可见${e?`: ${e}`:""}`,"帖子存在但当前账号看不到:多为仅作者可见、审核中或已删除。不是 id 抄错,也不是登录问题,重试无用。",l.EMPTY_RESULT)}export function parsePostRef(t){if(typeof t==="number"&&!Number.isSafeInteger(t))throw new f("牛客帖子 ID 已丢失精度,请传推荐结果中的原始字符串 ID");const e=String(t??"").trim(),n=e.match(/detail\/([0-9a-fA-F]{20,})/)?.[1]??e.match(/\/discuss\/(\d+)/)?.[1]??e;if(/^\d+$/.test(n))return{ref:n,form:BigInt(n)>BigInt(Number.MAX_SAFE_INTEGER)?"snowflake":"short"};if(/^[0-9a-fA-F]{20,}$/.test(n))return{ref:n,form:"uuid"};throw new f(`无法从输入解析牛客帖子 id: ${t}`,"可传 18 位讨论帖 id、32 位动态 uuid、7 位 momentId 或帖子链接。")}export async function resolvePostTarget(t,e){const{ref:n,form:o}=parsePostRef(e);if(o==="short")return{id:n,entityType:74,authorId:"",uuid:"",url:null,detail:null,liked:null,likeCount:null,totalComments:null,legacy:null};if(o==="snowflake"){const c=requireOk(await getJson(t,`${GW_BASE}/detail/content-data/detail/${n}`),"讨论帖解析",{id:e}).data;if(typeof c?.id==="number")throw new d("牛客讨论帖详情未返回匹配的原始字符串 ID");if(!c||c.id!==n)throw h(e,c?.showMessage?.message);const y=String(c.authorId??c.userBrief?.userId??"");if(!/^[1-9]\d*$/.test(y))throw new d("牛客讨论帖详情缺少作者 ID");const m=c.frequencyData??{};return{id:n,entityType:250,authorId:y,uuid:String(c.uuid??""),url:postUrl({entityType:250,id:n}),detail:c,liked:typeof m.isLike==="boolean"?m.isLike:null,likeCount:m.likeCnt??null,totalComments:m.totalCommentCnt??m.commentCnt??null,legacy:c.entityId&&c.entityType?{id:String(c.entityId),entityType:Number(c.entityType)}:null}}const r=requireOk(await getJson(t,`${GW_BASE}/detail/moment-data/detail/${n}`),"帖子解析",{id:e}).data;if(!r?.id)throw h(e,r?.showMessage?.message);const s=r.frequencyData??{},a=String(r.uuid??n);return{id:String(r.id),entityType:74,authorId:String(r.userId??""),uuid:a,url:postUrl({entityType:74,uuid:a}),detail:r,liked:typeof s.isLike==="boolean"?s.isLike:null,likeCount:s.likeCnt??null,totalComments:s.totalCommentCnt??s.commentCnt??null,legacy:null}}export async function readLikeState(t,e){const n=await resolvePostTarget(t,e);if(n.liked===null)throw new f(`读不到帖子 ${e} 当前是否已赞`,"7 位 momentId 没有可查询点赞状态的接口。请改传该动态的 uuid 或 /feed/main/detail/<uuid> 链接"+"(recommend / search / experience / my 的 uuid、url 列)。");return n}export async function resolveMomentId(t,e){const{ref:n,form:o}=parsePostRef(e);if(o!=="uuid")return n;return(await resolvePostTarget(t,e)).id}export async function resolveReplyTarget(t,e,n,o){if(!/^\d+$/.test(n))throw new f("--to-comment 必须是评论 ID");const r=requireOk(await getJson(t,`${GW_BASE}/detail/comment-terminal/${n}`),"回复目标").data;if(!r||String(r.id)!==n||!r.authorId)throw new d("牛客回复目标缺少评论 ID 或作者信息");const s=String(r.rootLongId??r.rootId??r.entryId??""),a=Number(r.rootType??r.entryType);if(s!==e.id||a!==e.entityType)throw new f("目标评论不属于指定帖子,已停止回复");const u=String(r.authorId);if(o!=="0"&&o!==u)throw new f("--to-user 与目标评论的实际作者不一致");const c=Number(r.entryType)===2?String(r.entryId):n;if(!/^\d+$/.test(c))throw new d("牛客回复目标缺少父评论 ID");return{id:c,entityType:2,authorId:u}}export function peerUidFromConversationId(t,e){const n=String(t??"").split("_");if(n.length!==2||!n.every((o)=>/^\d+$/.test(o)))return"";if(n[0]===n[1])return n[0];return n.find((o)=>o!==String(e))??""}export async function resolveConversation(t,e,n){const o=String(e??"").trim();if(!o)throw new d("缺少会话标识");if(o.includes("_")){const i=peerUidFromConversationId(o,n);if(!i)throw new d(`无法解析牛客会话 id: ${e}(应形如 <uidA>_<uidB>)`);return{conversationId:o,peerUid:i}}if(!/^\d+$/.test(o))throw new d(`无法解析牛客私信标识: ${e}(应为对方 userId 或 dm-list 的 conversation_id)`);for(let i=1;i<=5;i+=1){const s=requireOk(await getJson(t,`${WWW_BASE}/sns/message/${n}/get-conversation-list?page=${i}&_=${Date.now()}`),"私信会话列表").data?.conversations??[];if(!s.length)break;const a=s.find((u)=>String(u?.targetUserId??"")===o);if(a?.conversationId)return{conversationId:String(a.conversationId),peerUid:o}}throw new d(`在前 5 页会话列表里没找到与用户 ${o} 的会话(对方从未与你私信过时会话尚不存在,先用 dm-send 发起)`)}export function decodeHtmlEntities(t){return String(t??"").replace(/"/g,'"').replace(/�?39;/g,"'").replace(/'/g,"'").replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/&#(\d+);/g,(e,n)=>String.fromCodePoint(Number(n))).replace(/&/g,"&")}
|
package/clis/qiita/publish.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import*as c from"node:fs";import{cli as f,Strategy as m}from"@jackwener/opencli/registry";import{ArgumentError as
|
|
1
|
+
import*as c from"node:fs";import{cli as f,Strategy as m}from"@jackwener/opencli/registry";import{ArgumentError as n,CommandExecutionError as u}from"@jackwener/opencli/errors";import{qiitaGql as d,qiitaDeleteByForm as h}from"./gql.js";import{assertLiteralContent as b}from"../_shared/content-guard.js";export function resolveBody(t){if(t["body-file"]){const e=String(t["body-file"]);if(!c.statSync(e,{throwIfNoEntry:!1})?.isFile())throw new n(`--body-file not found: ${e}`);return c.readFileSync(e,"utf8")}if(t.body!==void 0&&t.body!==null){b(String(t.body),{fileFlag:"--body-file"});return String(t.body)}return}export function assertNoLocalImages(t){const e=[],a=(i)=>{const r=String(i??"").trim();if(!r)return;if(/^(https?:)?\/\//i.test(r))return;e.push(r.length>80?r.slice(0,80)+"…":r)};for(const i of t.matchAll(/!\[[^\]]*\]\(\s*<?([^)\s>]+)/g))a(i[1]);for(const i of t.matchAll(/<img\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi))a(i[1]);if(e.length)throw new n(`正文里有 ${e.length} 张非公网 URL 的图片(${e.slice(0,3).join("、")})。`+"Qiita 不转存本机图片,发出去会是裂图——请改用公网可直接访问的 http(s) 图片 URL,或移除这些图。")}export function resolveTagNames(t){return String(t??"").split(",").map((e)=>e.trim()).filter(Boolean)}const y=`mutation($input: SaveCreatingArticleInput!) {
|
|
2
2
|
saveCreatingArticle(input: $input) { draftItem { uuid } }
|
|
3
3
|
}`,w=`mutation($input: PublishPublicArticleInput!) {
|
|
4
4
|
publishPublicArticle(input: $input) { article { uuid linkUrl encryptedId isSecret } }
|
|
5
5
|
}`,q=`mutation($input: PublishSecretArticleInput!) {
|
|
6
6
|
publishSecretArticle(input: $input) { article { uuid linkUrl encryptedId isSecret } }
|
|
7
|
-
}`;f({site:"qiita",name:"publish",access:"write",description:"Publish a Qiita article (markdown). Tags required. Public by default; --draft or --secret to change.",domain:"qiita.com",strategy:m.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags (REQUIRED, at least 1)"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft only (do not publish)"},{name:"secret",type:"boolean",required:!1,default:!1,help:"Publish as limited-public (secret) instead of public"},{name:"tweet-share",type:"boolean",required:!1,default:!1,help:"Also share to X/Twitter on publish (public only)"}],columns:["status","uuid","url"],func:async(t,e)=>{if(!t)throw new
|
|
7
|
+
}`;f({site:"qiita",name:"publish",access:"write",description:"Publish a Qiita article (markdown). Tags required. Public by default; --draft or --secret to change.",domain:"qiita.com",strategy:m.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags (REQUIRED, at least 1)"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft only (do not publish)"},{name:"secret",type:"boolean",required:!1,default:!1,help:"Publish as limited-public (secret) instead of public"},{name:"tweet-share",type:"boolean",required:!1,default:!1,help:"Also share to X/Twitter on publish (public only)"}],columns:["status","uuid","url"],func:async(t,e)=>{if(!t)throw new u("Browser session required for qiita publish");const a=String(e.title??"").trim(),i=resolveBody(e),r=resolveTagNames(e.tags);if(!a)throw new n("--title is required");if(i===void 0)throw new n("--body or --body-file is required");if(r.length===0)throw new n("--tags is required (Qiita articles need at least one tag)");assertNoLocalImages(i);await t.goto("https://qiita.com/drafts/new");await t.wait({time:2});const o=await d(t,y,{input:{uuid:"",title:a,rawBody:i,tagNames:r,slide:!1,organizationId:null}}),l=o?.saveCreatingArticle?.draftItem?.uuid;if(!l)throw new u(`Qiita saveCreatingArticle returned no uuid: ${JSON.stringify(o)}`);if(e.draft)return[{status:"draft",uuid:l,url:`https://qiita.com/drafts/${l}/edit`}];let s;if(e.secret)s=(await d(t,q,{input:{uuid:l,title:a,rawBody:i,tagNames:r,slide:!1}}))?.publishSecretArticle?.article;else s=(await d(t,w,{input:{uuid:l,title:a,rawBody:i,tagNames:r,slide:!1,tweetShare:!!e["tweet-share"],adventCalendarItems:[]}}))?.publishPublicArticle?.article;if(!s||!s.linkUrl)throw new u(`Qiita publish returned no article: ${JSON.stringify(s)}`);return[{status:e.secret?"published-secret":"published",uuid:s.uuid||l,url:s.linkUrl}]}});f({site:"qiita",name:"delete",access:"write",description:"Delete a Qiita article or draft by its URL (article linkUrl, or https://qiita.com/drafts/<uuid>).",domain:"qiita.com",strategy:m.COOKIE,browser:!0,args:[{name:"target",type:"string",positional:!0,required:!1,help:"Article URL (https://qiita.com/<urlName>/items/<uuid>) or draft URL (https://qiita.com/drafts/<uuid>); same as --url"},{name:"url",type:"string",required:!1,help:"Article or draft URL (alternative to the positional argument)"}],columns:["status","url"],func:async(t,e)=>{if(!t)throw new u("Browser session required for qiita delete");const a=String(e.target??"").trim(),i=String(e.url??"").trim();if(a&&i&&a!==i)throw new n("Pass the URL once: either as the positional argument or with --url, not two different values");const r=i||a;if(!/^https:\/\/qiita\.com\/.+/.test(r))throw new n("A Qiita article or draft URL is required (not a bare item id)","Example: ppcli qiita delete https://qiita.com/<urlName>/items/<uuid>");await t.goto("https://qiita.com/");await t.wait({time:1});const o=await h(t,r);if(!o.ok)throw new u(`Qiita delete failed (HTTP ${o.status}) for ${r}`);return[{status:"deleted",url:r}]}});
|
|
@@ -36,4 +36,28 @@
|
|
|
36
36
|
<div class="comment-card"><a href="/profile/Commenter-One">Commenter One</a><a href="/comment/1">10h</a><p>A useful comment.</p><button aria-label="Upvote">Upvote 7</button><button>Reply</button></div>
|
|
37
37
|
<div class="comment-card"><a href="/profile/Commenter-Two">Commenter Two</a><a href="/comment/2">Mon</a><p>Another useful comment.</p><button aria-label="Upvote">Upvote</button><button>Reply</button></div>
|
|
38
38
|
</section>
|
|
39
|
+
<section id="fixture-question-related">
|
|
40
|
+
<div class="puppeteer_test_question_title"><span><span>Which assistant works best?</span></span></div>
|
|
41
|
+
<div class="dom_annotate_question_answer_item_0"><a href="/profile/Own-Author">Own Author</a><a class="answer_timestamp" href="/Which-assistant-works-best/answer/Own-Author">3d</a><div class="puppeteer_test_answer_content"><p>An answer to this very question.</p></div><button class="puppeteer_test_votable_upvote_button puppeteer_test_pressed" aria-label="Upvote">Upvote 1</button></div>
|
|
42
|
+
<div class="dom_annotate_question_answer_item_1"><span><span class="puppeteer_test_question_title"><div><div><div><div>Related</div></div></div></div><span><span>What is the best closing question?</span></span></span></span><a href="/profile/Other-Author">Other Author</a><a class="answer_timestamp" href="/What-is-the-best-closing-question/answer/Other-Author?no_redirect=1">9y</a><div class="puppeteer_test_answer_content"><p>An answer recommended from another question.</p></div><button class="puppeteer_test_votable_upvote_button" aria-label="Upvote">Upvote 3.5K</button></div>
|
|
43
|
+
</section>
|
|
44
|
+
<section id="fixture-search-spaces">
|
|
45
|
+
<div class="search-card"><a href="https://jobsadvice.quora.com/Can-I-use-Copilot-in-interviews"><div class="puppeteer_test_question_title">Can I use Copilot in interviews?</div></a><a href="https://jobsadvice.quora.com/Can-I-use-Copilot-in-interviews">1 answer</a></div>
|
|
46
|
+
<div class="search-card"><a href="/Can-I-use-Copilot-in-interviews"><div class="puppeteer_test_question_title">Can I use Copilot in interviews?</div></a><a href="/Can-I-use-Copilot-in-interviews">5 answers</a></div>
|
|
47
|
+
<div class="search-card"><a href="https://code.quora.com/Can-I-use-Copilot-in-interviews"><div class="puppeteer_test_question_title">Can I use Copilot in interviews?</div></a><a href="https://code.quora.com/Can-I-use-Copilot-in-interviews">No answer yet</a></div>
|
|
48
|
+
<div class="search-card"><a href="/unanswered/What-is-the-best-interview-copilot"><div class="puppeteer_test_question_title">What is the best interview copilot?</div></a><a href="/unanswered/What-is-the-best-interview-copilot">1 answer</a></div>
|
|
49
|
+
<div class="search-card"><a href="https://techies.quora.com/What-is-the-best-interview-copilot"><div class="puppeteer_test_question_title">What is the best interview copilot?</div></a></div>
|
|
50
|
+
<div class="search-card"><a href="https://onlyspace.quora.com/A-space-only-post"><div class="puppeteer_test_question_title">A space only post</div></a></div>
|
|
51
|
+
</section>
|
|
52
|
+
<section id="fixture-upvote-idle">
|
|
53
|
+
<a class="answer_timestamp" href="/Q/answer/User">Sep 17</a><p>Body.</p>
|
|
54
|
+
<div class="dom_annotate_answer_action_bar_upvote"><button class="puppeteer_test_votable_upvote_button" role="button" aria-label="Upvote" aria-disabled="false" tabindex="0"><div class="puppeteer_test_button_text">Upvote</div></button></div>
|
|
55
|
+
</section>
|
|
56
|
+
<section id="fixture-upvote-pressed">
|
|
57
|
+
<a class="answer_timestamp" href="/Q/answer/User">Aug 29</a><p>Body.</p>
|
|
58
|
+
<div class="dom_annotate_answer_action_bar_upvote"><button class="puppeteer_test_votable_upvote_button puppeteer_test_pressed" role="button" aria-label="Upvote" aria-disabled="false" tabindex="0"><div class="puppeteer_test_button_text">Upvote</div> 1</button></div>
|
|
59
|
+
</section>
|
|
60
|
+
<section id="fixture-upvote-missing">
|
|
61
|
+
<a class="answer_timestamp" href="/Q/answer/User">Aug 29</a><p>Body rendered before the action bar.</p>
|
|
62
|
+
</section>
|
|
39
63
|
</body></html>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CommandExecutionError as
|
|
1
|
+
import{CommandExecutionError as w,EmptyResultError as m}from"@jackwener/opencli/errors";import{cli as c,Strategy as d}from"@jackwener/opencli/registry";import{evaluateQuoraDom as p,normalizeQuoraUrl as f,openQuoraPage as h,waitForSelector as v}from"./shared.js";const i=["question","author","content","upvotes","upvoted","comments","shares","published","url"],y=8;c({site:"quora",name:"answer-detail",access:"read",description:"Read one complete Quora answer with question, author and engagement metrics.",domain:"www.quora.com",strategy:d.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora answer URL"}],columns:i,func:async(r,u)=>{const l=f(u.url,{allowProfile:!1,allowQuestion:!1,allowAnswer:!0});await h(r,l.url,"answer detail");await v(r,".puppeteer_test_question_title","answer question title");let o=null,n=null;for(let t=0;t<y;t++){o=await p(r,"answer-detail");const e=o?.rows?.[0],s=e?JSON.stringify([e.upvotes,e.upvoted,e.comments,e.shares]):null;if(e&&e.upvotes!==null&&s===n)break;n=s;await r.wait(1)}const a=o?.rows?.[0];if(!a)throw new m("quora answer-detail","The answer was not found.");if(!a.content)throw new w("Quora answer body was empty; the visible answer DOM may have changed.");return[Object.fromEntries(i.map((t)=>[t,a[t]??null]))]}});
|
package/clis/quora/question.js
CHANGED
|
@@ -1 +1,25 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{CommandExecutionError as f}from"@jackwener/opencli/errors";import{cli as h,Strategy as q}from"@jackwener/opencli/registry";import{evaluateQuoraDom as y,loadVisibleItems as x,normalizeLimit as b,normalizeQuoraUrl as v,openQuoraPage as g,parseCompactCount as A,QUORA_ORIGIN as _,waitForSelector as k}from"./shared.js";const C='[class*="dom_annotate_question_answer_item_"]';export function openAnswerFilterJs(){return`(() => {
|
|
2
|
+
const button = Array.from(document.querySelectorAll('#mainContent button[aria-haspopup="menu"]'))
|
|
3
|
+
.find((el) => /\\(\\s*[\\d.,]+\\s*[KMB]?\\s*\\)\\s*$/i.test((el.innerText || el.textContent || '').trim()));
|
|
4
|
+
if (!button) return false;
|
|
5
|
+
if (button.getAttribute('aria-expanded') !== 'true') button.click();
|
|
6
|
+
return true;
|
|
7
|
+
})()`}export function pickAnswerFilterJs(e){return`(() => {
|
|
8
|
+
const items = Array.from(document.querySelectorAll('.puppeteer_test_popover_item'))
|
|
9
|
+
.map((el) => ({ el, match: (el.innerText || el.textContent || '').trim().match(/\\(\\s*([\\d.,]+\\s*[KMB]?)\\s*\\)\\s*$/i) }))
|
|
10
|
+
.filter((item) => item.match);
|
|
11
|
+
if (!items.length) return { state: 'wait' };
|
|
12
|
+
const value = (text) => {
|
|
13
|
+
const m = String(text).replace(/,/g, '').match(/([\\d.]+)\\s*([KMB])?/i);
|
|
14
|
+
return m ? Number(m[1]) * ({ K: 1e3, M: 1e6, B: 1e9 }[String(m[2] || '').toUpperCase()] || 1) : NaN;
|
|
15
|
+
};
|
|
16
|
+
const counts = items.map((item) => item.match[1]);
|
|
17
|
+
let picked = 0;
|
|
18
|
+
for (let i = 1; i < items.length; i++) {
|
|
19
|
+
const smaller = value(counts[i]) < value(counts[picked]);
|
|
20
|
+
const larger = value(counts[i]) > value(counts[picked]);
|
|
21
|
+
if (${JSON.stringify(Boolean(e))} ? larger : smaller) picked = i;
|
|
22
|
+
}
|
|
23
|
+
items[picked].el.click();
|
|
24
|
+
return { state: 'done', counts, picked };
|
|
25
|
+
})()`}async function Q(e,o){if(!await e.evaluate(openAnswerFilterJs()))return null;for(let s=0;s<5;s++){await e.wait(1);const t=await e.evaluate(pickAnswerFilterJs(o));if(t?.state!=="done")continue;await e.wait(1);if(t.counts.length<2)return null;const i=t.counts.map((n)=>A(n)).filter((n)=>n!=null);return i.length?Math.min(...i):null}return null}h({site:"quora",name:"question",access:"read",description:"Read a Quora question: its own answer count and answers with engagement metrics. Answers to other (related) questions are excluded unless --related is set.",domain:"www.quora.com",strategy:q.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora question URL"},{name:"limit",type:"int",default:10,help:"Number of answers (max 50)"},{name:"related",type:"boolean",help:"Also list answers Quora recommends from other, related questions (type=related)"}],columns:["rank","type","question","answers","author","excerpt","upvotes","comments","shares","published","url"],func:async(e,o)=>{const p=v(o.url,{allowProfile:!1,allowQuestion:!0,allowAnswer:!1}),s=b(o.limit,10,50),t=o.related===!0,i=await g(e,p.url,"question");await k(e,".puppeteer_test_question_title","question title");const n=/^\/unanswered\//i.test(i.path);let a=n?0:await Q(e,t);if(!n||t)await x(e,C,s);const l=await y(e,"question");if(!l?.title)throw new f("Quora question title was empty; the visible question DOM may have changed.");const d=l.rows||[],u=n?[]:d.filter((r)=>!r.question),w=d.filter((r)=>r.question);if(a!=null&&u.length>a)a=u.length;const c=[{rank:0,type:"question",question:l.title,answers:a,author:null,excerpt:null,upvotes:null,comments:null,shares:null,published:null,url:`${_}${i.path}`}];u.slice(0,s).forEach((r,m)=>c.push({...r,rank:m+1,type:"answer",question:l.title,answers:null}));if(t)w.slice(0,s).forEach((r,m)=>c.push({...r,rank:m+1,type:"related",answers:null}));return c}});
|
package/clis/quora/search.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ArgumentError as
|
|
1
|
+
import{ArgumentError as m,EmptyResultError as w}from"@jackwener/opencli/errors";import{cli as q,Strategy as d}from"@jackwener/opencli/registry";import{evaluateQuoraDom as y,normalizeLimit as f,openQuoraPage as g,loadVisibleItems as b}from"./shared.js";q({site:"quora",name:"search",access:"read",description:"Search Quora questions or answers from the visible logged-in search results.",domain:"www.quora.com",strategy:d.COOKIE,browser:!0,args:[{name:"query",type:"string",required:!0,positional:!0,help:"Search query"},{name:"type",type:"string",default:"question",choices:["question","answer"],help:"Result type: question or answer"},{name:"limit",type:"int",default:10,help:"Number of results (max 50)"}],columns:["rank","type","title","author","excerpt","answers","upvotes","comments","shares","published","url"],func:async(s,n)=>{const a=String(n.query??"").trim();if(!a)throw new m("Search query is required");const t=String(n.type??"question");if(!["question","answer"].includes(t))throw new m("--type must be question or answer");const r=f(n.limit,10,50),p=`https://www.quora.com/search?q=${encodeURIComponent(a)}&type=${t}`;await g(s,p,"search");const h=t==="answer"?"search-answer":"search-question";let o=r,u=null;for(let e=0;e<4;e++){const l=await b(s,".puppeteer_test_question_title",o);u=await y(s,h);const c=u?.rows?.length||0;if(c>=r||l<o)break;o=l+(r-c)}const i=(u?.rows||[]).slice(0,r).map((e)=>({rank:e.rank,type:t,title:t==="answer"?e.question:e.title,author:t==="answer"?e.author:null,excerpt:t==="answer"?e.excerpt:null,answers:t==="question"?e.answers:null,upvotes:t==="answer"?e.upvotes:null,comments:t==="answer"?e.comments:null,shares:t==="answer"?e.shares:null,published:t==="answer"?e.published:null,url:e.url}));if(!i.length)throw new w("quora search",`No ${t} results for "${a}"; the page structure may have changed.`);return i}});
|
package/clis/quora/shared.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ArgumentError as x,AuthRequiredError as
|
|
1
|
+
import{ArgumentError as x,AuthRequiredError as C,CommandExecutionError as T}from"@jackwener/opencli/errors";import{readFile as R}from"node:fs/promises";export const QUORA_ORIGIN="https://www.quora.com";export async function hasQuoraAuthCookie(t){return(await t.getCookies({url:QUORA_ORIGIN})).some((e)=>e.name==="m-b"&&e.value)}export async function verifyQuoraAuth(t){if(!await hasQuoraAuthCookie(t))throw new C("www.quora.com","Quora m-b cookie 缺失——当前是游客/未登录会话");await t.goto(QUORA_ORIGIN);await t.wait(2);const u=await t.evaluate("location.pathname + location.search");if(/\blogin\b/i.test(String(u||"")))throw new C("www.quora.com",`Quora 停在登录页 (${u})——已登出`);if(!await hasQuoraAuthCookie(t))throw new C("www.quora.com","Quora m-b cookie 在导航后消失——会话失效");return{logged_in:!0}}export const EDITOR_MARK='[data-pp-quora-editor="1"]';export function tagEditorJs(){return`(() => {
|
|
2
2
|
const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
|
|
3
3
|
const scope = document.querySelector('[role="dialog"]') || document;
|
|
4
4
|
const cands = Array.from(scope.querySelectorAll('[contenteditable="true"]'))
|
|
@@ -13,24 +13,24 @@ import{ArgumentError as x,AuthRequiredError as S,CommandExecutionError as A}from
|
|
|
13
13
|
})()`}export function readEditorTextJs(){return`(() => {
|
|
14
14
|
const ed = document.querySelector('${EDITOR_MARK}');
|
|
15
15
|
return ed ? ('value' in ed ? ed.value : (ed.innerText || ed.textContent || '')) : null;
|
|
16
|
-
})()`}export function execInsertJs(
|
|
16
|
+
})()`}export function execInsertJs(t){return`(() => {
|
|
17
17
|
try {
|
|
18
18
|
const ed = document.querySelector('${EDITOR_MARK}');
|
|
19
19
|
if (!ed) return { ok: false, message: '编辑器已消失,无法写入。' };
|
|
20
20
|
ed.focus();
|
|
21
21
|
const normalize = (s) => String(s || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim();
|
|
22
|
-
if (!document.execCommand('insertText', false, ${JSON.stringify(
|
|
22
|
+
if (!document.execCommand('insertText', false, ${JSON.stringify(t)})) {
|
|
23
23
|
const dt = new DataTransfer();
|
|
24
|
-
dt.setData('text/plain', ${JSON.stringify(
|
|
24
|
+
dt.setData('text/plain', ${JSON.stringify(t)});
|
|
25
25
|
ed.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
|
|
26
26
|
}
|
|
27
27
|
const actual = 'value' in ed ? ed.value : (ed.innerText || ed.textContent || '');
|
|
28
|
-
if (normalize(actual).includes(normalize(${JSON.stringify(
|
|
28
|
+
if (normalize(actual).includes(normalize(${JSON.stringify(t)}))) return { ok: true };
|
|
29
29
|
return { ok: false, message: '写入后未在编辑器中校验到正文。', actualText: actual };
|
|
30
30
|
} catch (e) { return { ok: false, message: String(e) }; }
|
|
31
|
-
})()`}export async function insertEditorText(
|
|
31
|
+
})()`}export async function insertEditorText(t,u){const e=(l)=>String(l||"").replace(/ /g," ").replace(/\s+/g," ").trim(),r=[t.nativeType?.bind(t),t.insertText?.bind(t)].filter(Boolean);for(const l of r)try{await l(u);const i=await t.evaluate(readEditorTextJs());if(i!=null&&e(i).includes(e(u)))return{ok:!0}}catch(i){const y=(i instanceof Error?i.message:String(i)).toLowerCase();if(!(y.includes("unknown action")||y.includes("not supported")||y.includes("no inserted flag")))throw i}return t.evaluate(execInsertJs(u))}export function clickSubmitJs(t){return`(() => {
|
|
32
32
|
const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
|
|
33
|
-
const re = new RegExp(${JSON.stringify(
|
|
33
|
+
const re = new RegExp(${JSON.stringify(t.join("|"))}, 'i');
|
|
34
34
|
const scope = document.querySelector('[role="dialog"]') || document;
|
|
35
35
|
const btns = Array.from(scope.querySelectorAll('button, [role="button"]')).filter(visible);
|
|
36
36
|
// 文案匹配至多兜底:优先取未禁用且文案命中的按钮
|
|
@@ -41,18 +41,19 @@ import{ArgumentError as x,AuthRequiredError as S,CommandExecutionError as A}from
|
|
|
41
41
|
if (!hit) return { ok: false, message: '未找到可用的提交按钮(可能未渲染/被禁用/文案不在兜底名单)。' };
|
|
42
42
|
hit.click();
|
|
43
43
|
return { ok: true };
|
|
44
|
-
})()`}export function requireExecute(
|
|
44
|
+
})()`}export function requireExecute(t){if(t.execute!==!0)throw new x("Refusing to write without --execute. Review the target and content, then rerun with --execute.")}export async function resolveTextInput(t,{field:u="content",label:e="content"}={}){const r=t[u]==null?"":String(t[u]),l=t.file==null?"":String(t.file).trim();if(r&&l)throw new x(`Use either <${u}> or --file, not both`);let i=r;if(l)try{i=await R(l,"utf8")}catch(h){throw new x(`Cannot read --file ${l}: ${h instanceof Error?h.message:String(h)}`)}if(!i.trim())throw new x(`${e} cannot be empty`);return i}export function normalizeLimit(t,u=10,e=50){const r=t==null?u:Number(t);if(!Number.isInteger(r)||r<1)throw new x("--limit must be a positive integer");if(r>e)throw new x(`--limit must be <= ${e}`);return r}export function parseCompactCount(t){const e=String(t??"").trim().replace(/,/g,"").match(/(-?\d+(?:\.\d+)?)\s*([KMB])?/i);if(!e)return null;const r={K:1000,M:1e6,B:1e9}[String(e[2]||"").toUpperCase()]||1;return Math.round(Number(e[1])*r)}export function normalizeQuoraUrl(t,{allowProfile:u=!0,allowQuestion:e=!0,allowAnswer:r=!0}={}){const l=String(t??"").trim();if(!l)throw new x("A Quora URL is required");let i;try{i=new URL(l.startsWith("/")?`${QUORA_ORIGIN}${l}`:l)}catch{throw new x(`Invalid Quora URL: ${l}`)}if(i.protocol!=="https:"||i.username||i.password||i.port||!/(^|\.)quora\.com$/i.test(i.hostname))throw new x("URL must be an https://*.quora.com URL");const h=i.pathname.replace(/\/+$/,"")||"/",y=/^\/profile\/[^/]+$/i.test(h),b=/^\/(?!profile\/|topic\/|search$|answer$|stats$)[^/]+\/answers?\/[^/]+$/i.test(h),v=/^\/(?:unanswered\/)?(?!profile\/|topic\/|search$|answer$|stats$)[^/]+$/i.test(h);if((!u||!y)&&(!r||!b)&&(!e||!v))throw new x("Expected a Quora profile, question, or answer URL supported by this command");i.hash="";return{url:i.toString(),path:h,kind:y?"profile":b?"answer":"question"}}export function normalizeProfileTarget(t){const u=String(t??"").trim();if(!u)throw new x("Profile slug or URL is required");if(/^[^/?#\s]+$/.test(u))return{slug:u,url:`${QUORA_ORIGIN}/profile/${encodeURIComponent(u)}`};const e=normalizeQuoraUrl(u,{allowProfile:!0,allowQuestion:!1,allowAnswer:!1});return{slug:decodeURIComponent(e.path.split("/").pop()),url:e.url}}export async function waitForSelector(t,u,e,r=20){for(let l=0;l<r;l++){if(await t.evaluate(`(() => !!document.querySelector(${JSON.stringify(u)}))()`))return;await t.wait(1)}throw new T(`Timed out waiting for ${e}; Quora's visible page structure may have changed.`)}export function questionSlugPath(t){return String(t||"/").replace(/^\/unanswered(?=\/)/i,"")}export async function openQuoraPage(t,u,e){if(!t)throw new T(`Browser session required for quora ${e}`);try{await t.goto(u,{waitUntil:"none"})}catch(l){throw new T(`Failed to open Quora ${e}: ${l instanceof Error?l.message:String(l)}`)}const r=new URL(u).pathname.replace(/\/+$/,"")||"/";for(let l=0;l<20;l++){const i=await t.evaluate(`(() => ({
|
|
45
45
|
login: /\\blogin\\b/i.test((location.pathname || '') + (location.search || '')),
|
|
46
46
|
ready: !!document.querySelector('#mainContent'),
|
|
47
47
|
path: (location.pathname || '/').replace(/\\/+$/, '') || '/',
|
|
48
|
-
}))()`);if(
|
|
48
|
+
}))()`);if(i?.login)throw new C("www.quora.com",`Quora login is required for ${e}`);if(i?.ready&&(i.path===r||questionSlugPath(i.path)===questionSlugPath(r)))return{path:i.path};await t.wait(1)}throw new T(`Timed out opening Quora ${e}`)}export async function loadVisibleItems(t,u,e){let r=-1;for(let l=0;l<12;l++){const i=await t.evaluate(`(() => document.querySelectorAll(${JSON.stringify(u)}).length)()`);if(i>=e)return i;if(i===r&&l>=2)return i;r=i;await t.evaluate("(() => { window.scrollTo(0, document.body.scrollHeight); return true; })()");await t.wait(1)}return t.evaluate(`(() => document.querySelectorAll(${JSON.stringify(u)}).length)()`)}export function extractQuoraDom(t,u="#mainContent"){const e=document.querySelector(u);if(!e)return{ok:!1,reason:"main-content-missing",rows:[]};const r=(o)=>String(o??"").replace(/\u00a0/g," ").replace(/[ \t]+/g," ").replace(/\n{3,}/g,`
|
|
49
49
|
|
|
50
|
-
`).trim(),l=(
|
|
51
|
-
`).filter(Boolean);return{rank:
|
|
50
|
+
`).trim(),l=(o)=>{const n=r(o).replace(/,/g,"").match(/(-?\d+(?:\.\d+)?)\s*([KMB])?/i);if(!n)return null;const s={K:1000,M:1e6,B:1e9};return Math.round(Number(n[1])*(s[String(n[2]||"").toUpperCase()]||1))},i=(o)=>{if(!o)return"";try{return new URL(o,location.origin).toString()}catch{return""}},h=(o,n)=>{const s=n==="upvotes"?"button.puppeteer_test_votable_upvote_button":n==="comments"?".dom_annotate_answer_action_bar_comment button":".dom_annotate_answer_action_bar_share button",a=o.querySelector(s)||Array.from(o.querySelectorAll("button[aria-label]")).find((f)=>{const m=f.getAttribute("aria-label")||"";return n==="upvotes"?/upvot|赞同|votar|vote/i.test(m):n==="comments"?/comment|评论|comentario|commentaire|kommentar/i.test(m):/share|分享|compartir|partager|teilen/i.test(m)});if(!a)return null;const c=a.getAttribute("aria-label")||"";return l(`${c} ${a.innerText||a.textContent||""}`)||0},y=(o)=>{const n=o.querySelector("button.puppeteer_test_votable_upvote_button");if(!n)return null;return n.classList.contains("puppeteer_test_pressed")||n.getAttribute("aria-pressed")==="true"},b=(o)=>{if(!o)return"";const n=Array.from(o.children||[]),s=n.length>1&&n[0].tagName==="DIV"?n[n.length-1]:o;return r(s.innerText||s.textContent)},v=(o,n)=>{let s=o;while(s.parentElement&&s.parentElement!==e&&s.parentElement.querySelectorAll(n).length===1)s=s.parentElement;return s},E=()=>Array.from(e.querySelectorAll(".puppeteer_test_answer_content")).map((o,n)=>{const s=v(o,".puppeteer_test_answer_content"),p=Array.from(s.querySelectorAll('a[href*="/profile/"]')).find((m)=>r(m.innerText||m.textContent)),a=s.querySelector("a.answer_timestamp[href]"),c=s.querySelector(".puppeteer_test_question_title"),f=r(s.innerText||s.textContent).split(`
|
|
51
|
+
`).filter(Boolean);return{rank:n+1,question:b(c),author:r(p?.innerText||p?.textContent||f[0]||"Anonymous"),excerpt:r(o.innerText||o.textContent).replace(/\s*\(more\)\s*$/i,""),upvotes:h(s,"upvotes"),comments:h(s,"comments"),shares:h(s,"shares"),published:r(a?.innerText||a?.textContent),url:i(a?.getAttribute("href")||"")}});if(t==="question"||t==="answers"||t==="search-answer")return{ok:!0,title:b(e.querySelector(".puppeteer_test_question_title")),rows:E()};if(t==="upvote-state")return{ok:!0,found:!!e.querySelector("button.puppeteer_test_votable_upvote_button"),pressed:y(e),upvotes:h(e,"upvotes")};if(t==="search-question"){const o=new Map,n=[];for(const s of e.querySelectorAll(".puppeteer_test_question_title")){const p=s.closest("a[href]")||s.parentElement?.closest("a[href]"),a=i(p?.getAttribute("href")||"");if(!a)continue;const c=new URL(a),f=/^\/unanswered\//i.test(c.pathname),m=b(s),g=`${c.pathname.replace(/^\/unanswered(?=\/)/i,"").replace(/\/+$/,"").toLowerCase()}
|
|
52
|
+
${m.toLowerCase()}`,q=/^www\.quora\.com$/i.test(c.hostname),d=v(s,".puppeteer_test_question_title"),A=Array.from(d.querySelectorAll("a[href]")).map((S)=>r(S.innerText||S.textContent)).find((S)=>/answers?/i.test(S))||"",_=f?0:l(A)||0,w=o.get(g);if(w){if(q&&!w.canonical){w.row.url=a;w.row.answers=_;w.canonical=!0}continue}const k={rank:n.length+1,title:m,answers:_,url:a};o.set(g,{row:k,canonical:q});n.push(k)}return{ok:!0,rows:n}}if(t==="answer-detail"){const o=e.querySelector("a.answer_timestamp[href]"),n=e.querySelector("button.puppeteer_test_votable_upvote_button[aria-label]"),s=Array.from(e.querySelectorAll('a[href*="/profile/"]')).find((f)=>r(f.innerText||f.textContent)),p=e.querySelector(".puppeteer_test_question_title"),a=Array.from(e.querySelectorAll("p, li")).filter((f)=>{if(!o||!n)return!1;return Boolean(o.compareDocumentPosition(f)&4)&&Boolean(f.compareDocumentPosition(n)&4)}).map((f)=>r(f.innerText||f.textContent)).filter(Boolean),c=a.length?a.join(`
|
|
52
53
|
|
|
53
|
-
`):
|
|
54
|
-
`).map(
|
|
55
|
-
`).map(
|
|
56
|
-
`):
|
|
57
|
-
`).map(
|
|
58
|
-
`);
|
|
54
|
+
`):r(e.querySelector(".puppeteer_test_answer_content")?.innerText||e.querySelector(".puppeteer_test_answer_content")?.textContent);return{ok:!0,rows:[{question:b(p),author:r(s?.innerText||s?.textContent||"Anonymous"),content:c,upvotes:h(e,"upvotes"),upvoted:y(e),comments:h(e,"comments"),shares:h(e,"shares"),published:r(o?.innerText||o?.textContent),url:i(o?.getAttribute("href")||location.href)}]}}if(t==="answer-queue")return{ok:!0,rows:Array.from(e.querySelectorAll(".puppeteer_test_question_component_base")).map((n,s)=>{const p=n.querySelector(".puppeteer_test_question_title"),a=p?.closest("a[href]")||p?.parentElement?.closest("a[href]"),c=Array.from(n.querySelectorAll("a[href]")).map((f)=>r(f.innerText||f.textContent)).find((f)=>/answers?|no answer/i.test(f))||"";return{rank:s+1,title:r(p?.innerText||p?.textContent),answers:/no answer/i.test(c)?0:l(c)||0,url:i(a?.getAttribute("href")||"")}})};if(t==="profile"){const o=r(e.innerText||e.textContent),n=u==="#mainContent"?r(document.body.innerText||document.body.textContent):o,s=o.split(`
|
|
55
|
+
`).map(r).filter(Boolean),p=r(document.title.replace(/\s*-\s*Quora\s*$/i,""))||s[0]||"",a=Array.from(e.querySelectorAll("div, span")).map((m)=>r(m.innerText||m.textContent)).filter((m)=>m&&m.length<=300),c=(m)=>l(o.match(m)?.[1]||""),f=l(n.match(/([\d.,]+\s*[KMB]?)\s+content views/i)?.[1]||"");return{ok:!0,rows:[{name:p,bio:a.find((m)=>m!==p&&!/followers?|following|answers?|questions?|posts?|profile|content views|this month/i.test(m))||s.find((m)=>m!==p&&!/followers?|following|answers?|questions?|posts?|profile|content views|this month/i.test(m))||"",followers:c(/([\d.,]+\s*[KMB]?)\s+followers/i),following:c(/([\d.,]+\s*[KMB]?)\s+following/i),answers:c(/([\d.,]+\s*[KMB]?)\s+answers/i),questions:c(/([\d.,]+\s*[KMB]?)\s+questions/i),posts:c(/([\d.,]+\s*[KMB]?)\s+posts/i),contentViews:f,url:location.href.split(/[?#]/)[0].replace(/\/(?:answers|questions|posts|followers|following)\/?$/,"")}]}}if(t==="followers"){const o=new Set,n=[];for(const s of e.querySelectorAll('a[href*="/profile/"]')){const p=r(s.innerText||s.textContent),a=i(s.getAttribute("href")||"");if(!p||!a||o.has(a))continue;let c=s;while(c.parentElement&&c.parentElement!==e){const d=c.parentElement;if(new Set(Array.from(d.querySelectorAll('a[href*="/profile/"]'),(_)=>i(_.getAttribute("href")||""))).size>1)break;c=d;if(/\d[\d.,]*\s*[KMB]?\s+followers?/i.test(r(c.innerText||c.textContent)))break}const f=r(c.innerText||c.textContent),m=f.match(/([\d.,]+\s*[KMB]?)\s+followers?/i),g=f.split(`
|
|
56
|
+
`).map(r).filter(Boolean),q=Array.from(c.querySelectorAll("div, span")).map((d)=>r(d.innerText||d.textContent)).filter((d)=>d&&d.length<=300);n.push({rank:n.length+1,name:p,headline:q.find((d)=>d!==p&&!/followers?|^follow$/i.test(d))||g.find((d)=>d!==p&&!/followers?|^follow$/i.test(d))||"",followers:l(m?.[1]||"")||0,url:a});o.add(a)}return{ok:!0,rows:n}}if(t==="stats"){const o=r(e.innerText||e.textContent),n=(p)=>{const a=o.match(new RegExp(`${p}\\s*[–—-]?\\s*([\\d.,]+\\s*[KMB]?)`,"i"));return l(a?.[1]||"")||0};return{ok:!0,rows:[{period:o.match(/Last\s+\d+\s+days|All time/i)?.[0]||"Current",views:n("Views"),upvotes:n("Upvotes"),comments:n("Comments"),shares:n("Shares \\(external\\)")}]}}if(t==="comments"){const o=new Set,n=[],s=Array.from(e.querySelectorAll("button")).filter((p)=>/^reply$/i.test(r(p.innerText||p.textContent)));for(const p of s){let a=p;while(a.parentElement&&a.parentElement!==e){const w=a.parentElement;if(Array.from(w.querySelectorAll("button")).filter((S)=>/^reply$/i.test(r(S.innerText||S.textContent))).length!==1)break;a=w;if(a.querySelector('a[href*="/profile/"]'))break}const c=Array.from(a.querySelectorAll('a[href*="/profile/"]')).find((w)=>r(w.innerText||w.textContent)),f=r(c?.innerText||c?.textContent),m=i(c?.getAttribute("href")||""),g=r(a.innerText||a.textContent),q=`${m}:${g}`;if(!f||o.has(q))continue;const d=Array.from(a.querySelectorAll("a")).map((w)=>r(w.innerText||w.textContent)).find((w)=>/^(?:updated\s+)?(?:\d+[smhdwy]|mon|tue|wed|thu|fri|sat|sun|[A-Z][a-z]{2}\s+\d{1,2})$/i.test(w))||g.match(/(?:^|\s)(\d+[smhdwy]|mon|tue|wed|thu|fri|sat|sun)(?:\s|$)/i)?.[1]||"",A=Array.from(a.querySelectorAll("p")).map((w)=>r(w.innerText||w.textContent)).filter(Boolean),_=A.length?A.join(`
|
|
57
|
+
`):g.split(`
|
|
58
|
+
`).map(r).filter((w)=>w&&w!==f&&w!==d&&!/^reply$/i.test(w)).join(`
|
|
59
|
+
`);n.push({rank:n.length+1,author:f,content:_,upvotes:h(a,"upvotes"),published:d,authorUrl:m});o.add(q)}return{ok:!0,rows:n}}return{ok:!1,reason:`unsupported-mode:${t}`,rows:[]}}export async function evaluateQuoraDom(t,u,e="#mainContent"){return t.evaluate(`(${extractQuoraDom.toString()})(${JSON.stringify(u)}, ${JSON.stringify(e)})`)}
|