publishport-opencli 1.0.29 → 1.0.30
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 +464 -0
- package/clis/douyin/_shared/browser-fetch.js +31 -13
- package/clis/douyin/_shared/comments.js +1 -0
- package/clis/douyin/comment-like.js +1 -0
- package/clis/douyin/comment.js +1 -0
- package/clis/douyin/comments.js +1 -0
- package/clis/xiaohongshu/_shared/comment-actions.js +9 -0
- package/clis/xiaohongshu/_shared/comment-state.js +1 -0
- package/clis/xiaohongshu/comment-like.js +1 -0
- package/clis/xiaohongshu/comment.js +1 -0
- package/clis/xiaohongshu/comments.js +33 -5
- package/clis/xiaohongshu/replies.js +1 -0
- package/clis/xiaohongshu/reply.js +1 -0
- package/dist/src/cli.js +27 -25
- package/package.json +1 -1
- package/skills/opencli-browser/SKILL.md +14 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as h,Strategy as d}from"@jackwener/opencli/registry";import{AuthRequiredError as y,CommandExecutionError as a,EmptyResultError as g}from"@jackwener/opencli/errors";import{parseNoteId as w,buildNoteUrl as x}from"./note-helpers.js";import{readXhsDetailPage as b}from"./risk-control.js";import{readCommentPage as _}from"./_shared/comment-state.js";const k='.author-wrapper a[href*="/user/profile/"], a.name[href*="/user/profile/"], a.user-name[href*="/user/profile/"], a[href*="/user/profile/"]';export function parseXhsProfileHref(r,t="www.xiaohongshu.com"){const e=typeof r==="string"?r.trim():"";if(!e)return"";const n=String(t||"www.xiaohongshu.com").toLowerCase();let s;try{s=new URL(e,`https://${n}`)}catch{return""}if(s.protocol!=="https:")return"";if(s.hostname.toLowerCase()!==n)return"";return s.pathname.match(/^\/user\/profile\/([a-zA-Z0-9]+)\/?$/)?.[1]??""}export function buildXhsProfileUrl(r,t="www.xiaohongshu.com"){const e=parseXhsProfileHref(r,t);if(!e)return"";return`https://${t}/user/profile/${e}`}export function parseCommentLimit(r,t=20){const e=Number(r);if(!Number.isFinite(e))return t;return Math.max(1,Math.min(Math.floor(e),50))}export function parseXhsLikeCountText(r){const t=/^(?:\d+|\d{1,3}(?:[,,]\d{3})+)\+?$/u,e=/^((?:\d+|\d{1,3}(?:[,,]\d{3})+)(?:\.\d+)?)([wWkK万千])\+?$/u,n=String(r??"").replace(/\s+/g,"");if(!n)return 0;if(t.test(n))return Number(n.replace(/[,+,]/g,""));const s=n.match(e);if(!s)return 0;const i=Number(s[1].replace(/[,,]/g,""));if(!Number.isFinite(i))return 0;const l=s[2].toLowerCase();return Math.round(i*(l==="w"||l==="万"?1e4:1000))}function c(r,t,e){if(r==null)return"";if(typeof r!=="string")throw new a(`${e}: malformed comment row ${t}`);return r}export function normalizeCommentImages(r,t){if(r==null)return[];if(!Array.isArray(r))throw new a(`${t}: malformed comment row images`);const e=[];for(const n of r){if(typeof n!=="string")throw new a(`${t}: malformed comment row image URL`);const s=n.trim();let i;try{i=new URL(s)}catch{throw new a(`${t}: malformed comment row image URL`)}if(i.protocol!=="https:"&&i.protocol!=="http:"||i.username||i.password)throw new a(`${t}: malformed comment row image URL`);const l=i.toString();if(!e.includes(l))e.push(l)}return e}export function normalizeCommentRows(r,t="xiaohongshu/comments"){if(r==null)return[];if(!Array.isArray(r))throw new a(`${t}: malformed comments payload`);return r.map((e,n)=>{if(!e||typeof e!=="object"||Array.isArray(e))throw new a(`${t}: malformed comment row at index ${n}`);const s=c(e.text,"text",t);if(!s)throw new a(`${t}: malformed comment row text`);const i=Number(e.likes);if(!Number.isInteger(i)||i<0)throw new a(`${t}: malformed comment row likes`);if(typeof e.is_reply!=="boolean")throw new a(`${t}: malformed comment row is_reply`);return{id:e.id??null,root_id:e.root_id??null,parent_id:e.parent_id??null,liked:e.liked??null,reply_count:e.reply_count??null,replies_complete:e.replies_complete??null,author:c(e.author,"author",t),authorHrefRaw:c(e.authorHrefRaw,"authorHrefRaw",t),text:s,likes:i,time:c(e.time,"time",t),is_reply:e.is_reply,reply_to:c(e.reply_to,"reply_to",t),images:normalizeCommentImages(e.images,t)}})}export function buildCommentsExtractJs(r,t=20,e=null){const n=parseXhsLikeCountText.toString();return`
|
|
2
2
|
(async () => {
|
|
3
3
|
const wait = (ms) => new Promise(r => setTimeout(r, ms))
|
|
4
4
|
const withReplies = ${r}
|
|
@@ -48,11 +48,11 @@ import{cli as p,Strategy as d}from"@jackwener/opencli/registry";import{AuthRequi
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
|
|
51
|
-
const parseLikeCountText = ${
|
|
51
|
+
const parseLikeCountText = ${n}
|
|
52
52
|
const parseLikes = (el) => {
|
|
53
53
|
return parseLikeCountText(clean(el))
|
|
54
54
|
}
|
|
55
|
-
const HREF_SELECTOR = ${JSON.stringify(
|
|
55
|
+
const HREF_SELECTOR = ${JSON.stringify(k)}
|
|
56
56
|
const extractAuthorHref = (el) => {
|
|
57
57
|
if (!el) return ''
|
|
58
58
|
const anchor = el.querySelector(HREF_SELECTOR)
|
|
@@ -75,6 +75,21 @@ import{cli as p,Strategy as d}from"@jackwener/opencli/registry";import{AuthRequi
|
|
|
75
75
|
}
|
|
76
76
|
const expandReplyThreads = async (root) => {
|
|
77
77
|
if (!withReplies || !root) return
|
|
78
|
+
if (${JSON.stringify(e)} !== null) {
|
|
79
|
+
for (let round = 0; round < 20; round++) {
|
|
80
|
+
const el = root.querySelector('.show-more')
|
|
81
|
+
if (!el) break
|
|
82
|
+
const before = root.querySelectorAll('.comment-item-sub').length
|
|
83
|
+
el.click()
|
|
84
|
+
let progressed = false
|
|
85
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
86
|
+
await wait(300)
|
|
87
|
+
if (root.querySelectorAll('.comment-item-sub').length > before) { progressed = true; break }
|
|
88
|
+
}
|
|
89
|
+
if (!progressed) break
|
|
90
|
+
}
|
|
91
|
+
return
|
|
92
|
+
}
|
|
78
93
|
const clickedTexts = new Set()
|
|
79
94
|
for (let round = 0; round < 3; round++) {
|
|
80
95
|
const expanders = Array.from(root.querySelectorAll('button, [role="button"], span, div')).filter(el => {
|
|
@@ -96,7 +111,8 @@ import{cli as p,Strategy as d}from"@jackwener/opencli/registry";import{AuthRequi
|
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
const results = []
|
|
99
|
-
const
|
|
114
|
+
const allParents = Array.from(document.querySelectorAll('.parent-comment'))
|
|
115
|
+
const parents = ${JSON.stringify(e)} === null ? allParents : allParents.slice(0, targetCount)
|
|
100
116
|
for (const p of parents) {
|
|
101
117
|
const item = p.querySelector('.comment-item')
|
|
102
118
|
if (!item) continue
|
|
@@ -110,6 +126,7 @@ import{cli as p,Strategy as d}from"@jackwener/opencli/registry";import{AuthRequi
|
|
|
110
126
|
|
|
111
127
|
if (!text) continue
|
|
112
128
|
results.push({ author, authorHrefRaw, text, likes, time, is_reply: false, reply_to: '', images })
|
|
129
|
+
if (${JSON.stringify(e)} !== null) results[results.length - 1].id = item.id.replace(/^comment-/, '')
|
|
113
130
|
|
|
114
131
|
// Extract nested replies (楼中楼)
|
|
115
132
|
if (withReplies) {
|
|
@@ -123,10 +140,21 @@ import{cli as p,Strategy as d}from"@jackwener/opencli/registry";import{AuthRequi
|
|
|
123
140
|
const sImages = extractImages(sub)
|
|
124
141
|
if (!sText) return
|
|
125
142
|
results.push({ author: sAuthor, authorHrefRaw: sAuthorHrefRaw, text: sText, likes: sLikes, time: sTime, is_reply: true, reply_to: author, images: sImages })
|
|
143
|
+
if (${JSON.stringify(e)} !== null) results[results.length - 1].id = sub.id.replace(/^comment-/, '')
|
|
126
144
|
})
|
|
127
145
|
}
|
|
128
146
|
}
|
|
129
147
|
|
|
148
|
+
if (${JSON.stringify(e)} !== null && !loginWall && !securityBlock) {
|
|
149
|
+
const state = (${_.toString()})(${JSON.stringify(e)})
|
|
150
|
+
if (state.error) return { pageUrl: location.href, securityBlock, loginWall, identityError: state.error }
|
|
151
|
+
for (const row of results) {
|
|
152
|
+
const entry = state.rows.find(c => c.id === row.id)
|
|
153
|
+
if (!entry) return { pageUrl: location.href, securityBlock, loginWall, identityError: 'comment identity unavailable' }
|
|
154
|
+
Object.assign(row, { root_id: entry.root_id, parent_id: entry.parent_id, liked: entry.liked,
|
|
155
|
+
reply_count: entry.reply_count, replies_complete: entry.replies_complete, reply_to: entry.reply_to })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
130
158
|
return { pageUrl: location.href, securityBlock, loginWall, results }
|
|
131
159
|
})()
|
|
132
|
-
`}export const command=
|
|
160
|
+
`}export const command=h({site:"xiaohongshu",name:"comments",access:"read",description:"获取小红书笔记评论(支持楼中楼子回复)",domain:"www.xiaohongshu.com",strategy:d.COOKIE,navigateBefore:!1,args:[{name:"note-id",required:!0,positional:!0,help:"Full Xiaohongshu note URL with xsec_token"},{name:"limit",type:"int",default:20,help:"Number of top-level comments (max 50)"},{name:"with-replies",type:"boolean",default:!1,help:"Include nested replies (楼中楼)"}],columns:["rank","id","root_id","parent_id","author","userId","profileUrl","text","likes","liked","time","is_reply","reply_to","reply_count","replies_complete","images"],func:async(r,t)=>{const e=parseCommentLimit(t.limit),n=Boolean(t["with-replies"]),s=String(t["note-id"]),i=w(s),l=await b(r,{url:x(s,{commandName:"xiaohongshu comments"}),extractJs:buildCommentsExtractJs(n,e,i),securityHelp:/^https?:\/\//.test(s)?"The page may be temporarily restricted. Try again later or from a different session.":"Try using a full URL from search results (with xsec_token) instead of a bare note ID."});if(!l||typeof l!=="object")throw new g("xiaohongshu/comments","Unexpected evaluate response");if(l.loginWall)throw new y("www.xiaohongshu.com","Note comments require login");if(l.identityError)throw new a(`xiaohongshu/comments: ${l.identityError}`);const m=normalizeCommentRows(l.results,"xiaohongshu/comments"),p=(o,u)=>({rank:u+1,id:o.id,root_id:o.root_id,parent_id:o.parent_id,author:o.author,userId:o.authorHrefRaw?parseXhsProfileHref(o.authorHrefRaw):"",profileUrl:o.authorHrefRaw?buildXhsProfileUrl(o.authorHrefRaw):"",text:o.text,likes:o.likes,liked:o.liked,time:o.time,is_reply:o.is_reply,reply_to:o.reply_to,reply_count:o.reply_count,replies_complete:o.replies_complete,images:o.images??[]});if(n){const o=[];let u=0;for(const f of m){if(!f.is_reply)u++;if(u>e)break;o.push(f)}return o.map(p)}return m.slice(0,e).map(p)}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as p,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as a}from"@jackwener/opencli/errors";import{commentId as w,commentNote as h,expandCommentThread as y,locateComment as g,openCommentPage as _}from"./_shared/comment-state.js";p({site:"xiaohongshu",name:"replies",access:"read",description:"读取指定主评论下的楼中楼,返回精确回复关系与加载完整性",domain:"www.xiaohongshu.com",strategy:f.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"note-id",positional:!0,required:!0,help:"Full signed note URL with xsec_token"},{name:"root-id",positional:!0,required:!0,help:"Top-level comment ID from comments"},{name:"limit",type:"int",default:50,help:"Maximum nested replies to return (1-200)"}],columns:["id","reply_count","returned","complete","replies"],func:async(r,i)=>{const{url:c,noteId:n}=h(i["note-id"]),e=w(i["root-id"],"root-id"),t=Number(i.limit??50);if(!Number.isInteger(t)||t<1||t>200)throw new a("--limit must be an integer from 1 to 200");await _(r,c,n);const{target:d}=await g(r,n,e);if(d.is_reply)throw new a("root-id must identify a top-level comment");const s=await y(r,n,e,t),m=s.rows.find((o)=>o.id===e),l=s.rows.filter((o)=>o.is_reply&&o.root_id===e).slice(0,t).map(({rendered:o,...u})=>u);return[{id:e,reply_count:m.reply_count,returned:l.length,complete:m.replies_complete&&l.length>=m.reply_count,replies:l}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as r,Strategy as o}from"@jackwener/opencli/registry";import{runComment as i,writeArgs as n,writeColumns as m}from"./_shared/comment-actions.js";r({site:"xiaohongshu",name:"reply",access:"write",description:"回复指定小红书评论(含楼中楼);默认 dry-run,--execute 才发送",domain:"www.xiaohongshu.com",strategy:o.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"note-id",positional:!0,required:!0,help:"Full signed note URL with xsec_token"},{name:"comment-id",positional:!0,required:!0,help:"Exact comment ID from comments/replies"},{name:"text",positional:!0,required:!0,help:"Exact plain-text reply"},...n],columns:m,func:(t,e)=>i(t,{...e,"reply-to":e["comment-id"]})});
|