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.
@@ -1,4 +1,4 @@
1
- import{cli as p,Strategy as d}from"@jackwener/opencli/registry";import{AuthRequiredError as g,CommandExecutionError as a,EmptyResultError as w}from"@jackwener/opencli/errors";import{parseNoteId as y,buildNoteUrl as x}from"./note-helpers.js";import{readXhsDetailPage as b}from"./risk-control.js";const R='.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 s=String(t||"www.xiaohongshu.com").toLowerCase();let o;try{o=new URL(e,`https://${s}`)}catch{return""}if(o.protocol!=="https:")return"";if(o.hostname.toLowerCase()!==s)return"";return o.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,s=String(r??"").replace(/\s+/g,"");if(!s)return 0;if(t.test(s))return Number(s.replace(/[,+,]/g,""));const o=s.match(e);if(!o)return 0;const n=Number(o[1].replace(/[,,]/g,""));if(!Number.isFinite(n))return 0;const l=o[2].toLowerCase();return Math.round(n*(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 s of r){if(typeof s!=="string")throw new a(`${t}: malformed comment row image URL`);const o=s.trim();let n;try{n=new URL(o)}catch{throw new a(`${t}: malformed comment row image URL`)}if(n.protocol!=="https:"&&n.protocol!=="http:"||n.username||n.password)throw new a(`${t}: malformed comment row image URL`);const l=n.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,s)=>{if(!e||typeof e!=="object"||Array.isArray(e))throw new a(`${t}: malformed comment row at index ${s}`);const o=c(e.text,"text",t);if(!o)throw new a(`${t}: malformed comment row text`);const n=Number(e.likes);if(!Number.isInteger(n)||n<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{author:c(e.author,"author",t),authorHrefRaw:c(e.authorHrefRaw,"authorHrefRaw",t),text:o,likes:n,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){const e=parseXhsLikeCountText.toString();return`
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 = ${e}
51
+ const parseLikeCountText = ${n}
52
52
  const parseLikes = (el) => {
53
53
  return parseLikeCountText(clean(el))
54
54
  }
55
- const HREF_SELECTOR = ${JSON.stringify(R)}
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 parents = document.querySelectorAll('.parent-comment')
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=p({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","author","userId","profileUrl","text","likes","time","is_reply","reply_to","images"],func:async(r,t)=>{const e=parseCommentLimit(t.limit),s=Boolean(t["with-replies"]),o=String(t["note-id"]),n=y(o),l=await b(r,{url:x(o,{commandName:"xiaohongshu comments"}),extractJs:buildCommentsExtractJs(s,e),securityHelp:/^https?:\/\//.test(o)?"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 w("xiaohongshu/comments","Unexpected evaluate response");if(l.loginWall)throw new g("www.xiaohongshu.com","Note comments require login");const m=normalizeCommentRows(l.results,"xiaohongshu/comments"),f=(i,u)=>({rank:u+1,author:i.author,userId:i.authorHrefRaw?parseXhsProfileHref(i.authorHrefRaw):"",profileUrl:i.authorHrefRaw?buildXhsProfileUrl(i.authorHrefRaw):"",text:i.text,likes:i.likes,time:i.time,is_reply:i.is_reply,reply_to:i.reply_to,images:i.images??[]});if(s){const i=[];let u=0;for(const h of m){if(!h.is_reply)u++;if(u>e)break;i.push(h)}return i.map(f)}return m.slice(0,e).map(f)}});
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"]})});