publishport-opencli 1.0.48 → 1.0.49

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 CHANGED
@@ -31443,7 +31443,6 @@
31443
31443
  "content",
31444
31444
  "id",
31445
31445
  "momentId",
31446
- "uuid",
31447
31446
  "entityType",
31448
31447
  "url",
31449
31448
  "likes",
@@ -52687,7 +52686,6 @@
52687
52686
  "parent_id",
52688
52687
  "author",
52689
52688
  "userId",
52690
- "profileUrl",
52691
52689
  "text",
52692
52690
  "likes",
52693
52691
  "liked",
@@ -57364,7 +57362,7 @@
57364
57362
  {
57365
57363
  "site": "zhihu",
57366
57364
  "name": "question",
57367
- "description": "知乎问题详情和回答",
57365
+ "description": "知乎问题详情和回答(回答正文为 200 字摘要,截断时带 content_len,全文用 zhihu answer-detail <id>;题目元数据只写在第一行)",
57368
57366
  "access": "read",
57369
57367
  "domain": "www.zhihu.com",
57370
57368
  "strategy": "cookie",
@@ -57403,6 +57401,7 @@
57403
57401
  "votes",
57404
57402
  "url",
57405
57403
  "content",
57404
+ "content_len",
57406
57405
  "question_title",
57407
57406
  "question_answers",
57408
57407
  "question_followers",
@@ -1 +1 @@
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}});
1
+ import{cli as c,Strategy as u}from"@jackwener/opencli/registry";import{ArgumentError as l,CommandExecutionError as p}from"@jackwener/opencli/errors";import{GW_BASE as d,feedRecordToPost as y,noteTrustedEmpty as h,postJson as f,requireOk as w}from"./utils.js";c({site:"nowcoder",name:"search",access:"read",description:"Full-text search",domain:"www.nowcoder.com",strategy:u.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","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,`${d}/pc/search`,{query:i,type:String(o.type??"all"),page:1,pageSize:r}),"搜索"),n=a.data?.records;if(!Array.isArray(n))throw new p("牛客搜索未返回 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,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}});
@@ -1,4 +1,4 @@
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`
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 o=String(t||"www.xiaohongshu.com").toLowerCase();let s;try{s=new URL(e,`https://${o}`)}catch{return""}if(s.protocol!=="https:")return"";if(s.hostname.toLowerCase()!==o)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,o=String(r??"").replace(/\s+/g,"");if(!o)return 0;if(t.test(o))return Number(o.replace(/[,+,]/g,""));const s=o.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 o of r){if(typeof o!=="string")throw new a(`${t}: malformed comment row image URL`);const s=o.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,o)=>{if(!e||typeof e!=="object"||Array.isArray(e))throw new a(`${t}: malformed comment row at index ${o}`);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 compactCommentRow(r){const t={};for(const[e,o]of Object.entries(r)){if(o===null||o===void 0||o==="")continue;if(Array.isArray(o)&&o.length===0)continue;if(e==="root_id"&&o===r.id)continue;t[e]=o}return t}export function buildCommentsExtractJs(r,t=20,e=null){const o=parseXhsLikeCountText.toString();return`
2
2
  (async () => {
3
3
  const wait = (ms) => new Promise(r => setTimeout(r, ms))
4
4
  const withReplies = ${r}
@@ -48,7 +48,7 @@ import{cli as h,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 = ${n}
51
+ const parseLikeCountText = ${o}
52
52
  const parseLikes = (el) => {
53
53
  return parseLikeCountText(clean(el))
54
54
  }
@@ -157,4 +157,4 @@ import{cli as h,Strategy as d}from"@jackwener/opencli/registry";import{AuthRequi
157
157
  }
158
158
  return { pageUrl: location.href, securityBlock, loginWall, results }
159
159
  })()
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)}});
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","text","likes","liked","time","is_reply","reply_to","reply_count","replies_complete","images"],func:async(r,t)=>{const e=parseCommentLimit(t.limit),o=Boolean(t["with-replies"]),s=String(t["note-id"]),i=w(s),l=await b(r,{url:x(s,{commandName:"xiaohongshu comments"}),extractJs:buildCommentsExtractJs(o,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=(n,u)=>compactCommentRow({rank:u+1,id:n.id,root_id:n.root_id,parent_id:n.parent_id,author:n.author,userId:n.authorHrefRaw?parseXhsProfileHref(n.authorHrefRaw):"",text:n.text,likes:n.likes,liked:n.liked,time:n.time,is_reply:n.is_reply,reply_to:n.reply_to,reply_count:n.reply_count,replies_complete:n.replies_complete,images:n.images??[]});if(o){const n=[];let u=0;for(const f of m){if(!f.is_reply)u++;if(u>e)break;n.push(f)}return n.map(p)}return m.slice(0,e).map(p)}});
@@ -1 +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}]}});
1
+ import{cli as u,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as p}from"@jackwener/opencli/errors";import{commentId as w,commentNote as h,expandCommentThread as y,locateComment as _,openCommentPage as g}from"./_shared/comment-state.js";u({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:a,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 p("--limit must be an integer from 1 to 200");await g(r,a,n);const{target:c}=await _(r,n,e);if(c.is_reply)throw new p("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,profileUrl:x,root_id:I,is_reply:b,reply_count:C,replies_complete:v,...d})=>d);return[{id:e,reply_count:m.reply_count,returned:l.length,complete:m.replies_complete&&l.length>=m.reply_count,replies:l}]}});
@@ -1,4 +1,4 @@
1
- import{cli as g,Strategy as y}from"@jackwener/opencli/registry";import{CliError as h}from"@jackwener/opencli/errors";import{stripHtml as I}from"./text.js";import{fetchZhihuJson as b}from"./read-shared.js";function C(t){if(typeof t!=="string")return"";try{const e=new URL(t);if(e.hostname!=="www.zhihu.com"&&e.hostname!=="zhihu.com")return"";return e.pathname.match(/^\/question\/\d+\/answer\/(\d+)\/?$/)?.[1]||e.pathname.match(/^\/api\/v4\/answers\/(\d+)\/?$/)?.[1]||e.pathname.match(/^\/answer\/(\d+)\/?$/)?.[1]||""}catch{return""}}function q(t){const e=C(t.url);if(e)return e;if(typeof t.id==="string"&&/^\d+$/.test(t.id))return t.id;if(typeof t.id==="number"&&Number.isSafeInteger(t.id)&&t.id>0)return String(t.id);return""}function v(t){const e=q(t);if(e)return`id:${e}`;return`fallback:${t.author?.name||"anonymous"}:${t.content||""}`}const _=1000;function N(t){return`
1
+ import{cli as C,Strategy as b}from"@jackwener/opencli/registry";import{CliError as d}from"@jackwener/opencli/errors";import{stripHtml as v}from"./text.js";import{fetchZhihuJson as N}from"./read-shared.js";function E(t){if(typeof t!=="string")return"";try{const e=new URL(t);if(e.hostname!=="www.zhihu.com"&&e.hostname!=="zhihu.com")return"";return e.pathname.match(/^\/question\/\d+\/answer\/(\d+)\/?$/)?.[1]||e.pathname.match(/^\/api\/v4\/answers\/(\d+)\/?$/)?.[1]||e.pathname.match(/^\/answer\/(\d+)\/?$/)?.[1]||""}catch{return""}}function g(t){const e=E(t.url);if(e)return e;if(typeof t.id==="string"&&/^\d+$/.test(t.id))return t.id;if(typeof t.id==="number"&&Number.isSafeInteger(t.id)&&t.id>0)return String(t.id);return""}function z(t){const e=g(t);if(e)return`id:${e}`;return`fallback:${t.author?.name||"anonymous"}:${t.content||""}`}const I=1000,y=200;function S(t){return`
2
2
  (() => {
3
3
  try {
4
4
  const el = document.getElementById('js-initialData');
@@ -17,5 +17,5 @@ import{cli as g,Strategy as y}from"@jackwener/opencli/registry";import{CliError
17
17
  return null;
18
18
  }
19
19
  })()
20
- `}function z(t){const e=t&&typeof t==="object"&&"session"in t&&"data"in t?t.data:t;if(!e||typeof e!=="object")return null;const u=(a)=>typeof a==="number"&&Number.isFinite(a)?a:null;return{title:typeof e.title==="string"?e.title:"",answerCount:u(e.answerCount),followerCount:u(e.followerCount),visitCount:u(e.visitCount)}}g({site:"zhihu",name:"question",access:"read",description:"知乎问题详情和回答",domain:"www.zhihu.com",strategy:y.COOKIE,args:[{name:"id",required:!0,positional:!0,help:"Question ID (numeric)"},{name:"limit",type:"int",default:5,help:"Number of answers (max 1000; use normal-sized requests)"},{name:"sort",default:"default",choices:["default","created"],help:"Answer order: default or created"}],columns:["rank","id","author","votes","url","content","question_title","question_answers","question_followers","question_views"],func:async(t,e)=>{const{id:u,limit:a=5}=e,i=String(u);if(!/^\d+$/.test(i))throw new h("INVALID_INPUT","Question ID must be numeric","Example: ppcli zhihu question 123456789");const l=Number(a);if(!Number.isInteger(l)||l<=0||l>_)throw new h("INVALID_INPUT",`Limit must be a positive integer no greater than ${_}`,"Use a normal-sized limit to avoid slow requests or Zhihu risk controls");const d=String(e.sort||"default");if(d!=="default"&&d!=="created")throw new h("INVALID_INPUT","Sort must be one of: default, created","Example: ppcli zhihu question 123456789 --sort created");await t.goto(d==="created"?`https://www.zhihu.com/question/${i}/answers/updated`:`https://www.zhihu.com/question/${i}`);let c=`https://www.zhihu.com/api/v4/questions/${i}/answers?limit=${20}&offset=0&sort_by=${d}&include=data[*].content,url,voteup_count,comment_count,author`;const f=[],w=new Set,p=new Set;while(c&&f.length<l&&!p.has(c)){p.add(c);const n=await b(t,c,{label:"问题回答列表"});if(!n||n.__httpError||n.__malformedJson){const r=n?.__httpError;if(r===404)throw new h("NOT_FOUND",`知乎问题 ${i} 的回答接口返回 HTTP 404:该问题 ID 可能不存在`,"核对问题 ID 是否抄错");throw new h("FETCH_ERROR",r?`Zhihu question answers request failed (HTTP ${r})`:"Zhihu question answers request failed","Try again later or rerun with -v for more detail")}for(const r of n.data||[]){const s=v(r);if(w.has(s))continue;w.add(s);f.push(r);if(f.length>=l)break}if(n.paging?.is_end)break;c=typeof n.paging?.next==="string"?n.paging.next:""}let m=null;try{m=await t.evaluate(N(i))}catch{m=null}const o=z(m);if(f.length===0){const n=o?`题目「${o.title}」,回答数 ${o.answerCount??"未知"},关注 ${o.followerCount??"未知"},浏览 ${o.visitCount??"未知"}`:"未能从页面读到该题的回答数";process.stderr.write(`zhihu question ${i}: 接口返回 0 个回答(${n})。空列表是真实结果,不是取数失败。
21
- `)}return f.map((n,r)=>{const s=q(n);return{rank:r+1,id:s,author:n.author?.name||"anonymous",votes:n.voteup_count||0,url:s?`https://www.zhihu.com/question/${i}/answer/${s}`:"",content:I(n.content||"").substring(0,200),question_title:o?.title??"",question_answers:o?.answerCount??null,question_followers:o?.followerCount??null,question_views:o?.visitCount??null}})}});
20
+ `}function T(t){const e=t&&typeof t==="object"&&"session"in t&&"data"in t?t.data:t;if(!e||typeof e!=="object")return null;const u=(a)=>typeof a==="number"&&Number.isFinite(a)?a:null;return{title:typeof e.title==="string"?e.title:"",answerCount:u(e.answerCount),followerCount:u(e.followerCount),visitCount:u(e.visitCount)}}C({site:"zhihu",name:"question",access:"read",description:"知乎问题详情和回答(回答正文为 200 字摘要,截断时带 content_len,全文用 zhihu answer-detail <id>;题目元数据只写在第一行)",domain:"www.zhihu.com",strategy:b.COOKIE,args:[{name:"id",required:!0,positional:!0,help:"Question ID (numeric)"},{name:"limit",type:"int",default:5,help:"Number of answers (max 1000; use normal-sized requests)"},{name:"sort",default:"default",choices:["default","created"],help:"Answer order: default or created"}],columns:["rank","id","author","votes","url","content","content_len","question_title","question_answers","question_followers","question_views"],func:async(t,e)=>{const{id:u,limit:a=5}=e,r=String(u);if(!/^\d+$/.test(r))throw new d("INVALID_INPUT","Question ID must be numeric","Example: ppcli zhihu question 123456789");const l=Number(a);if(!Number.isInteger(l)||l<=0||l>I)throw new d("INVALID_INPUT",`Limit must be a positive integer no greater than ${I}`,"Use a normal-sized limit to avoid slow requests or Zhihu risk controls");const f=String(e.sort||"default");if(f!=="default"&&f!=="created")throw new d("INVALID_INPUT","Sort must be one of: default, created","Example: ppcli zhihu question 123456789 --sort created");await t.goto(f==="created"?`https://www.zhihu.com/question/${r}/answers/updated`:`https://www.zhihu.com/question/${r}`);let c=`https://www.zhihu.com/api/v4/questions/${r}/answers?limit=${20}&offset=0&sort_by=${f}&include=data[*].content,url,voteup_count,comment_count,author`;const h=[],p=new Set,q=new Set;while(c&&h.length<l&&!q.has(c)){q.add(c);const n=await N(t,c,{label:"问题回答列表"});if(!n||n.__httpError||n.__malformedJson){const i=n?.__httpError;if(i===404)throw new d("NOT_FOUND",`知乎问题 ${r} 的回答接口返回 HTTP 404:该问题 ID 可能不存在`,"核对问题 ID 是否抄错");throw new d("FETCH_ERROR",i?`Zhihu question answers request failed (HTTP ${i})`:"Zhihu question answers request failed","Try again later or rerun with -v for more detail")}for(const i of n.data||[]){const s=z(i);if(p.has(s))continue;p.add(s);h.push(i);if(h.length>=l)break}if(n.paging?.is_end)break;c=typeof n.paging?.next==="string"?n.paging.next:""}let m=null;try{m=await t.evaluate(S(r))}catch{m=null}const o=T(m);if(h.length===0){const n=o?`题目「${o.title}」,回答数 ${o.answerCount??"未知"},关注 ${o.followerCount??"未知"},浏览 ${o.visitCount??"未知"}`:"未能从页面读到该题的回答数";process.stderr.write(`zhihu question ${r}: 接口返回 0 个回答(${n})。空列表是真实结果,不是取数失败。
21
+ `)}return h.map((n,i)=>{const s=g(n),w=v(n.content||""),_=w.length>y;return{rank:i+1,id:s,author:n.author?.name||"anonymous",votes:n.voteup_count||0,url:s?`https://www.zhihu.com/question/${r}/answer/${s}`:"",content:_?`${w.substring(0,y)}…`:w,..._?{content_len:w.length}:{},...i===0?{question_title:o?.title??"",question_answers:o?.answerCount??null,question_followers:o?.followerCount??null,question_views:o?.visitCount??null}:{}}})}});
@@ -46,5 +46,13 @@ interface AuthRefreshOptions {
46
46
  }
47
47
  export declare function collectAuthStatus(options: AuthStatusOptions): Promise<AuthStatusRow[]>;
48
48
  export declare function collectAuthRefresh(options: AuthRefreshOptions): Promise<AuthRefreshRow[]>;
49
+ export interface AuthStatusSummaryRow {
50
+ status: AuthStatus;
51
+ count: number;
52
+ sites: string;
53
+ note: string;
54
+ checked_at: string;
55
+ }
56
+ export declare function summarizeAuthStatus(rows: AuthStatusRow[]): AuthStatusSummaryRow[];
49
57
  export declare function registerAuthCommands(program: Command): Command;
50
58
  export {};
@@ -1,3 +1,4 @@
1
- import{mkdir as W,readFile as D,rename as le,writeFile as U}from"node:fs/promises";import{homedir as H}from"node:os";import{dirname as V,join as z}from"node:path";import{pathToFileURL as fe}from"node:url";import{InvalidArgumentError as _,Option as de}from"commander";import{AuthRequiredError as L,CliError as G,getErrorMessage as R}from"../errors.js";import{executeCommand as w}from"../execution.js";import{ensureBrowserBridgeReady as he}from"../browser/daemon-lifecycle.js";import{resolveProfile as ge}from"../browser/profile.js";import{isElectronApp as E}from"../electron-apps.js";import{fullName as me,getRegistry as J}from"../registry.js";import{render as K}from"../output.js";const x=1,T=86400000;function k(e,t,r){if(e===void 0||e===null||e==="")return r;const i=Number(e);if(!Number.isInteger(i)||i<=0)throw new _(`${t} must be a positive integer. Received: "${String(e)}"`);return i}function Q(e,t,r){if(e===void 0||e===null||e==="")return r;const i=Number(e);if(!Number.isInteger(i)||i<0)throw new _(`${t} must be a non-negative integer. Received: "${String(e)}"`);return i}function Y(e){if(!e||!e.trim())return null;const t=e.split(",").map((r)=>r.trim()).filter(Boolean);return t.length>0?new Set(t):null}function pe(){return z(H(),".ppcli","auth-refresh.json")}const C=1,ye=600000;function _e(){return z(H(),".ppcli","auth-state.json")}async function we(e){try{const t=JSON.parse(await D(e,"utf8"));if(t&&t.version===C&&t.profiles&&typeof t.profiles==="object")return{version:C,profiles:t.profiles}}catch{}return{version:C,profiles:{}}}async function ke(e,t){await W(V(e),{recursive:!0});const r=`${e}.${process.pid}.tmp`;await U(r,`${JSON.stringify(t,null,2)}
2
- `,"utf8");await le(r,e)}function X(e,t){return e!==void 0&&t>=e&&t-e<ye}function Se(e,t,r){return{site:e,status:t.status,logged_in:t.logged_in,identity:t.identity,checked:"cached",error:t.error,checked_at:new Date(t.at).toISOString(),verified:X(t.verifiedAt,r)}}function be(e,t,r,i){return{status:e.status,logged_in:e.logged_in,identity:e.identity,checked:e.checked,error:e.error,at:t,...r!==void 0?{verifiedAt:r}:{},...i!==void 0?{verifyAttemptAt:i}:{}}}function Ae(){return{version:x,sites:{}}}async function ve(e){try{const t=JSON.parse(await D(e,"utf8"));if(t&&t.version===x&&t.sites&&typeof t.sites==="object")return{version:x,sites:t.sites}}catch(t){if(t.code!=="ENOENT")throw t}return Ae()}async function Re(e,t){await W(V(e),{recursive:!0});await U(e,`${JSON.stringify(t,null,2)}
3
- `,"utf8")}function Z(e){if(!e?.last_touched_at)return null;const t=Date.parse(e.last_touched_at);return Number.isFinite(t)?t:null}function Ee(e,t){const r=Z(e);return r!==null&&t.getTime()-r<T}function h(e){const t=Z(e);return t===null?"":new Date(t+T).toISOString()}function ee(){const e=new Set;return[...J().values()].filter((t)=>{if(e.has(t))return!1;e.add(t);return t.name==="whoami"&&t.access==="read"}).sort((t,r)=>t.site.localeCompare(r.site))}async function O(e){const t=e;if(!t._lazy||!t._modulePath)return e;await import(fe(t._modulePath).href);return J().get(me(e))??e}function S(e,t){const r=e.args.some((i)=>i.name==="timeout");return{...e,args:r?e.args:[...e.args,{name:"timeout",type:"int",default:t,help:"Per-site auth command timeout in seconds"}]}}function xe(e,t){if(e.browser!==!0||typeof e.authStatus?.quickCheck!=="function")return null;return S({...e,func:e.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},t)}function te(e){if(typeof e==="boolean")return e;if(e&&typeof e==="object"&&!Array.isArray(e)){const t=e.logged_in;if(typeof t==="boolean")return t}return null}function Te(e){const t=Array.isArray(e)?e[0]:e;if(t&&typeof t==="object"&&!Array.isArray(t)){const r=t.logged_in;if(typeof r==="boolean")return r}return null}function Ce(e){const t=Array.isArray(e)?e[0]:e;if(t&&typeof t==="object"&&!Array.isArray(t)){const r=t.error;if(typeof r==="string"&&r.trim())return r.trim()}return""}function re(e){if(e===void 0||e===null)return"";if(typeof e==="string"||typeof e==="number"||typeof e==="boolean")return String(e);return""}function ne(e){const t=Array.isArray(e)?e[0]:e;if(!t||typeof t!=="object"||Array.isArray(t))return"";const r=t,i=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const n of["username","handle","user_id","id","name","nickname","user_type","url"]){if(i.test(n))continue;const s=re(r[n]);if(s)return s}for(const[n,s]of Object.entries(r)){if(n==="site"||n==="logged_in"||i.test(n))continue;const a=re(s);if(a)return a}return""}function I(e){return e instanceof L||typeof e==="object"&&e!==null&&e.code==="AUTH_REQUIRED"}function ie(e,t,r){if(I(r))return{site:e,status:"not_logged_in",logged_in:!1,identity:"",checked:t,error:""};const i=r instanceof G?r.code:"",n=R(r);return{site:e,status:"error",logged_in:"",identity:"",checked:t,error:i?`${i}: ${n}`:n}}function Oe(e,t){if(e.browser!==!0)return null;let r=e.authStatus?.refresh;if(typeof r!=="function"){const i=e.authStatus?.quickCheck;if(typeof i!=="function"||!e.domain)return null;const n=e.domain.startsWith("http://")||e.domain.startsWith("https://")?e.domain:`https://${e.domain}`;r=async(s,a,u)=>{await s.goto(n);await s.wait(1);if(te(await i(s,a,u))!==!0)throw new L(e.domain??e.site,`Auth refresh quickCheck failed for ${e.site}`);return{status:"touched"}}}return S({...e,func:r,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},t)}function Ie(e){if(e&&typeof e==="object"&&!Array.isArray(e)){const t=e;if(t.status==="refreshed"||t.refreshed===!0)return"refreshed"}return"touched"}function qe(e,t,r){if(I(r))return{site:e,status:"not_logged_in",last_touched_at:t?.last_touched_at??"",next_refresh_at:h(t),error:""};const i=r instanceof G?r.code:"",n=R(r);return{site:e,status:"error",last_touched_at:t?.last_touched_at??"",next_refresh_at:h(t),error:i?`${i}: ${n}`:n}}function oe(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"false"}}async function Ne(e,t){try{const r=await O(e);if(r.browser!==!0){const a=S(r,t.timeoutSeconds),u=await w(a,{timeout:t.timeoutSeconds},!1,{...t.profile?{profile:t.profile}:{}});return{site:e.site,status:"logged_in",logged_in:!0,identity:ne(u),checked:"quick",error:""}}if(r.authStatus?.quickCheckRequiresNavigation===!0)return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"navigation_required",error:""};const i=xe(r,t.timeoutSeconds);if(!i)return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const n=await w(i,{timeout:t.timeoutSeconds},!1,{...oe(),windowMode:"background",...t.profile?{profile:t.profile}:{}}),s=te(n);if(s===!0)return{site:e.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(s===!1)return{site:e.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(r){return ie(e.site,"quick",r)}}async function Me(e,t){try{const r=await O(e),i=S(r,t.timeoutSeconds),n=await w(i,{timeout:t.timeoutSeconds},!1,{...oe(),windowMode:"background",...t.profile?{profile:t.profile}:{}});if(Te(n)===!1)return{site:e.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"full",error:Ce(n)};return{site:e.site,status:"logged_in",logged_in:!0,identity:ne(n),checked:"full",error:""}}catch(r){return ie(e.site,"full",r)}}function Fe(e,t){return t?`${e}@${t}`:e}async function $e(e,t){const r=Fe(e.site,t.profile),i=t.state.sites[r];if(!t.force&&Ee(i,t.now))return{site:e.site,status:"skipped",last_touched_at:i?.last_touched_at??"",next_refresh_at:h(i),error:""};const n=t.now.toISOString();try{const s=await O(e),a=Oe(s,t.timeoutSeconds);if(!a){t.state.sites[r]={...i,last_attempt_at:n,last_status:"unsupported"};return{site:e.site,status:"unsupported",last_touched_at:i?.last_touched_at??"",next_refresh_at:h(i),error:"refresh probe is not available for this site"}}const u=await w(a,{timeout:t.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"false",windowMode:"background",...t.profile?{profile:t.profile}:{}}),d=Ie(u);t.state.sites[r]={...i,last_attempt_at:n,last_touched_at:n,last_status:d};return{site:e.site,status:d,last_touched_at:n,next_refresh_at:new Date(t.now.getTime()+T).toISOString(),error:""}}catch(s){const a=I(s)?"not_logged_in":"error";t.state.sites[r]={...i,last_attempt_at:n,last_status:a};return qe(e.site,i,s)}}async function se(e,t){const r=ge(t);if(r.kind!=="extension")return null;if(!e.some((n)=>n.browser===!0&&!E(n.site)))return null;try{await he({timeoutSeconds:15,contextId:r.contextId,verbose:!1});return null}catch(n){return R(n)}}function Pe(e,t,r){return{site:e,status:"error",logged_in:"",identity:"",checked:t,error:`BROWSER_CONNECT: ${r}`}}async function ae(e,t,r){const i=Array(e.length);let n=0;const s=Array.from({length:Math.min(t,e.length)},async()=>{while(n<e.length){const a=n++;i[a]=await r(e[a])}});await Promise.all(s);return i}export async function collectAuthStatus(e){const t=Y(e.sites),r=e.full?"full":"quick",i=k(e.concurrency,"--concurrency",r==="full"?3:8),n=k(e.timeout,"--timeout",r==="full"?20:8),s=String(e.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes(s))throw new _("--only must be one of: all, logged-in, not-logged-in, unknown, error");if(r==="full"&&!t)throw new _("--full 会对每个站点真实打开页面,不允许全量使用。"+"要实证某个站点请点名:auth status --site <site> --full;"+"想看全部站点就用默认的 quick(纯 cookie,不开任何页面)。");const a=Q(e.maxAge,"--max-age",0)*1000,u=Q(e.verify,"--verify",0),d=e.statePath??_e(),f=(e.now??new Date).getTime(),l=e.profile??"",b=ee().filter((o)=>!t||t.has(o.site)),A=await we(d),g=A.profiles[l]??{},v=new Map,m=[];for(const o of b){const c=g[o.site];if(r!=="full"&&a>0&&c&&f>=c.at&&f-c.at<a)v.set(o.site,Se(o.site,c,f));else m.push(o)}const q=m.length>0?await se(m,e.profile):null,ue=await ae(m,i,(o)=>q&&o.browser===!0&&!E(o.site)?Promise.resolve(Pe(o.site,r,q)):r==="full"?Me(o,{timeoutSeconds:n,profile:e.profile}):Ne(o,{timeoutSeconds:n,profile:e.profile})),je=new Map(b.map((o)=>[o.site,o])),p=new Map(v),N=new Map,M=new Map;for(const o of ue){p.set(o.site,o);if(r==="full"){M.set(o.site,f);if(o.status!=="error")N.set(o.site,f)}}const F=(o)=>{const c=N.get(o.site);if(c!==void 0)return c;const y=g[o.site];return y&&y.status===o.status&&X(y.verifiedAt,f)?y.verifiedAt:void 0},ce=(o)=>M.get(o.site)??g[o.site]?.verifyAttemptAt,P={...g};for(const o of p.keys()){if(v.has(o))continue;const c=p.get(o);if(!c)continue;P[o]=be(c,f,F(c),ce(c))}A.profiles[l]=P;await ke(d,A).catch(()=>{});const j=b.map((o)=>p.get(o.site)).filter((o)=>o!==void 0).map((o)=>({...o,checked_at:o.checked_at??new Date(f).toISOString(),verified:o.verified??F(o)!==void 0})),B=s.replace(/-/g,"_");return B==="all"?j:j.filter((o)=>o.status===B)}export async function collectAuthRefresh(e){const t=Y(e.sites),r=k(e.concurrency,"--concurrency",3),i=k(e.timeout,"--timeout",20),n=e.statePath??pe(),s=e.now??new Date,a=await ve(n),u=ee().filter((l)=>!t||t.has(l.site)),d=await se(u,e.profile),f=await ae(u,r,(l)=>d&&l.browser===!0&&!E(l.site)?Promise.resolve({site:l.site,status:"error",last_touched_at:a.sites[l.site]?.last_touched_at??"",next_refresh_at:h(a.sites[l.site]),error:`BROWSER_CONNECT: ${d}`}):$e(l,{timeoutSeconds:i,profile:e.profile,now:s,state:a,force:e.all===!0}));await Re(n,a);return f}export function registerAuthCommands(e){const t=e.command("auth").description("Inspect website login status"),r=t.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").option("--max-age <seconds>","Reuse a local cached verdict younger than this instead of probing").option("--verify <n>","Deprecated and ignored: batch auth status never navigates. Use --site <s> --full to confirm one site with a real whoami").addOption(new de("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(n)=>{const s=typeof r.optsWithGlobals==="function"?r.optsWithGlobals():{},a=await collectAuthStatus({sites:n.site,full:n.full===!0,concurrency:n.concurrency,timeout:n.timeout,only:n.only,maxAge:n.maxAge,verify:n.verify,profile:typeof s.profile==="string"&&s.profile.trim()?s.profile.trim():void 0}),u=typeof n.format==="string"?n.format:"table";K(a,{fmt:u,fmtExplicit:r.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"ppcli/auth status",source:n.full?"full whoami probe":"quick auth check"})}),i=t.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(n)=>{const s=typeof i.optsWithGlobals==="function"?i.optsWithGlobals():{},a=await collectAuthRefresh({sites:n.site,all:n.all===!0,concurrency:n.concurrency,timeout:n.timeout,profile:typeof s.profile==="string"&&s.profile.trim()?s.profile.trim():void 0}),u=typeof n.format==="string"?n.format:"table";K(a,{fmt:u,fmtExplicit:i.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"ppcli/auth refresh",source:n.all?"forced persistent touch":"persistent touch with 24h throttle"})});return t}
1
+ import{mkdir as W,readFile as D,rename as fe,writeFile as H}from"node:fs/promises";import{homedir as V}from"node:os";import{dirname as z,join as L}from"node:path";import{pathToFileURL as de}from"node:url";import{InvalidArgumentError as _,Option as he}from"commander";import{AuthRequiredError as G,CliError as J,getErrorMessage as R}from"../errors.js";import{executeCommand as w}from"../execution.js";import{ensureBrowserBridgeReady as me}from"../browser/daemon-lifecycle.js";import{resolveProfile as ge}from"../browser/profile.js";import{isElectronApp as E}from"../electron-apps.js";import{fullName as pe,getRegistry as K}from"../registry.js";import{render as O}from"../output.js";const x=1,T=86400000;function k(e,t,r){if(e===void 0||e===null||e==="")return r;const o=Number(e);if(!Number.isInteger(o)||o<=0)throw new _(`${t} must be a positive integer. Received: "${String(e)}"`);return o}function Q(e,t,r){if(e===void 0||e===null||e==="")return r;const o=Number(e);if(!Number.isInteger(o)||o<0)throw new _(`${t} must be a non-negative integer. Received: "${String(e)}"`);return o}function Y(e){if(!e||!e.trim())return null;const t=e.split(",").map((r)=>r.trim()).filter(Boolean);return t.length>0?new Set(t):null}function ye(){return L(V(),".ppcli","auth-refresh.json")}const C=1,_e=600000;function we(){return L(V(),".ppcli","auth-state.json")}async function ke(e){try{const t=JSON.parse(await D(e,"utf8"));if(t&&t.version===C&&t.profiles&&typeof t.profiles==="object")return{version:C,profiles:t.profiles}}catch{}return{version:C,profiles:{}}}async function Se(e,t){await W(z(e),{recursive:!0});const r=`${e}.${process.pid}.tmp`;await H(r,`${JSON.stringify(t,null,2)}
2
+ `,"utf8");await fe(r,e)}function X(e,t){return e!==void 0&&t>=e&&t-e<_e}function be(e,t,r){return{site:e,status:t.status,logged_in:t.logged_in,identity:t.identity,checked:"cached",error:t.error,checked_at:new Date(t.at).toISOString(),verified:X(t.verifiedAt,r)}}function Ae(e,t,r,o){return{status:e.status,logged_in:e.logged_in,identity:e.identity,checked:e.checked,error:e.error,at:t,...r!==void 0?{verifiedAt:r}:{},...o!==void 0?{verifyAttemptAt:o}:{}}}function ve(){return{version:x,sites:{}}}async function Re(e){try{const t=JSON.parse(await D(e,"utf8"));if(t&&t.version===x&&t.sites&&typeof t.sites==="object")return{version:x,sites:t.sites}}catch(t){if(t.code!=="ENOENT")throw t}return ve()}async function Ee(e,t){await W(z(e),{recursive:!0});await H(e,`${JSON.stringify(t,null,2)}
3
+ `,"utf8")}function Z(e){if(!e?.last_touched_at)return null;const t=Date.parse(e.last_touched_at);return Number.isFinite(t)?t:null}function Oe(e,t){const r=Z(e);return r!==null&&t.getTime()-r<T}function h(e){const t=Z(e);return t===null?"":new Date(t+T).toISOString()}function ee(){const e=new Set;return[...K().values()].filter((t)=>{if(e.has(t))return!1;e.add(t);return t.name==="whoami"&&t.access==="read"}).sort((t,r)=>t.site.localeCompare(r.site))}async function I(e){const t=e;if(!t._lazy||!t._modulePath)return e;await import(de(t._modulePath).href);return K().get(pe(e))??e}function S(e,t){const r=e.args.some((o)=>o.name==="timeout");return{...e,args:r?e.args:[...e.args,{name:"timeout",type:"int",default:t,help:"Per-site auth command timeout in seconds"}]}}function xe(e,t){if(e.browser!==!0||typeof e.authStatus?.quickCheck!=="function")return null;return S({...e,func:e.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},t)}function te(e){if(typeof e==="boolean")return e;if(e&&typeof e==="object"&&!Array.isArray(e)){const t=e.logged_in;if(typeof t==="boolean")return t}return null}function Te(e){const t=Array.isArray(e)?e[0]:e;if(t&&typeof t==="object"&&!Array.isArray(t)){const r=t.logged_in;if(typeof r==="boolean")return r}return null}function Ce(e){const t=Array.isArray(e)?e[0]:e;if(t&&typeof t==="object"&&!Array.isArray(t)){const r=t.error;if(typeof r==="string"&&r.trim())return r.trim()}return""}function re(e){if(e===void 0||e===null)return"";if(typeof e==="string"||typeof e==="number"||typeof e==="boolean")return String(e);return""}function ne(e){const t=Array.isArray(e)?e[0]:e;if(!t||typeof t!=="object"||Array.isArray(t))return"";const r=t,o=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const n of["username","handle","user_id","id","name","nickname","user_type","url"]){if(o.test(n))continue;const s=re(r[n]);if(s)return s}for(const[n,s]of Object.entries(r)){if(n==="site"||n==="logged_in"||o.test(n))continue;const a=re(s);if(a)return a}return""}function q(e){return e instanceof G||typeof e==="object"&&e!==null&&e.code==="AUTH_REQUIRED"}function oe(e,t,r){if(q(r))return{site:e,status:"not_logged_in",logged_in:!1,identity:"",checked:t,error:""};const o=r instanceof J?r.code:"",n=R(r);return{site:e,status:"error",logged_in:"",identity:"",checked:t,error:o?`${o}: ${n}`:n}}function Ie(e,t){if(e.browser!==!0)return null;let r=e.authStatus?.refresh;if(typeof r!=="function"){const o=e.authStatus?.quickCheck;if(typeof o!=="function"||!e.domain)return null;const n=e.domain.startsWith("http://")||e.domain.startsWith("https://")?e.domain:`https://${e.domain}`;r=async(s,a,u)=>{await s.goto(n);await s.wait(1);if(te(await o(s,a,u))!==!0)throw new G(e.domain??e.site,`Auth refresh quickCheck failed for ${e.site}`);return{status:"touched"}}}return S({...e,func:r,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},t)}function qe(e){if(e&&typeof e==="object"&&!Array.isArray(e)){const t=e;if(t.status==="refreshed"||t.refreshed===!0)return"refreshed"}return"touched"}function Me(e,t,r){if(q(r))return{site:e,status:"not_logged_in",last_touched_at:t?.last_touched_at??"",next_refresh_at:h(t),error:""};const o=r instanceof J?r.code:"",n=R(r);return{site:e,status:"error",last_touched_at:t?.last_touched_at??"",next_refresh_at:h(t),error:o?`${o}: ${n}`:n}}function ie(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"false"}}async function Ne(e,t){try{const r=await I(e);if(r.browser!==!0){const a=S(r,t.timeoutSeconds),u=await w(a,{timeout:t.timeoutSeconds},!1,{...t.profile?{profile:t.profile}:{}});return{site:e.site,status:"logged_in",logged_in:!0,identity:ne(u),checked:"quick",error:""}}if(r.authStatus?.quickCheckRequiresNavigation===!0)return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"navigation_required",error:""};const o=xe(r,t.timeoutSeconds);if(!o)return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const n=await w(o,{timeout:t.timeoutSeconds},!1,{...ie(),windowMode:"background",...t.profile?{profile:t.profile}:{}}),s=te(n);if(s===!0)return{site:e.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(s===!1)return{site:e.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:e.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(r){return oe(e.site,"quick",r)}}async function $e(e,t){try{const r=await I(e),o=S(r,t.timeoutSeconds),n=await w(o,{timeout:t.timeoutSeconds},!1,{...ie(),windowMode:"background",...t.profile?{profile:t.profile}:{}});if(Te(n)===!1)return{site:e.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"full",error:Ce(n)};return{site:e.site,status:"logged_in",logged_in:!0,identity:ne(n),checked:"full",error:""}}catch(r){return oe(e.site,"full",r)}}function Pe(e,t){return t?`${e}@${t}`:e}async function Be(e,t){const r=Pe(e.site,t.profile),o=t.state.sites[r];if(!t.force&&Oe(o,t.now))return{site:e.site,status:"skipped",last_touched_at:o?.last_touched_at??"",next_refresh_at:h(o),error:""};const n=t.now.toISOString();try{const s=await I(e),a=Ie(s,t.timeoutSeconds);if(!a){t.state.sites[r]={...o,last_attempt_at:n,last_status:"unsupported"};return{site:e.site,status:"unsupported",last_touched_at:o?.last_touched_at??"",next_refresh_at:h(o),error:"refresh probe is not available for this site"}}const u=await w(a,{timeout:t.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"false",windowMode:"background",...t.profile?{profile:t.profile}:{}}),d=qe(u);t.state.sites[r]={...o,last_attempt_at:n,last_touched_at:n,last_status:d};return{site:e.site,status:d,last_touched_at:n,next_refresh_at:new Date(t.now.getTime()+T).toISOString(),error:""}}catch(s){const a=q(s)?"not_logged_in":"error";t.state.sites[r]={...o,last_attempt_at:n,last_status:a};return Me(e.site,o,s)}}async function se(e,t){const r=ge(t);if(r.kind!=="extension")return null;if(!e.some((n)=>n.browser===!0&&!E(n.site)))return null;try{await me({timeoutSeconds:15,contextId:r.contextId,verbose:!1});return null}catch(n){return R(n)}}function Fe(e,t,r){return{site:e,status:"error",logged_in:"",identity:"",checked:t,error:`BROWSER_CONNECT: ${r}`}}async function ae(e,t,r){const o=Array(e.length);let n=0;const s=Array.from({length:Math.min(t,e.length)},async()=>{while(n<e.length){const a=n++;o[a]=await r(e[a])}});await Promise.all(s);return o}export async function collectAuthStatus(e){const t=Y(e.sites),r=e.full?"full":"quick",o=k(e.concurrency,"--concurrency",r==="full"?3:8),n=k(e.timeout,"--timeout",r==="full"?20:8),s=String(e.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes(s))throw new _("--only must be one of: all, logged-in, not-logged-in, unknown, error");if(r==="full"&&!t)throw new _("--full 会对每个站点真实打开页面,不允许全量使用。"+"要实证某个站点请点名:auth status --site <site> --full;"+"想看全部站点就用默认的 quick(纯 cookie,不开任何页面)。");const a=Q(e.maxAge,"--max-age",0)*1000,u=Q(e.verify,"--verify",0),d=e.statePath??we(),f=(e.now??new Date).getTime(),l=e.profile??"",b=ee().filter((i)=>!t||t.has(i.site)),A=await ke(d),m=A.profiles[l]??{},v=new Map,g=[];for(const i of b){const c=m[i.site];if(r!=="full"&&a>0&&c&&f>=c.at&&f-c.at<a)v.set(i.site,be(i.site,c,f));else g.push(i)}const M=g.length>0?await se(g,e.profile):null,ce=await ae(g,o,(i)=>M&&i.browser===!0&&!E(i.site)?Promise.resolve(Fe(i.site,r,M)):r==="full"?$e(i,{timeoutSeconds:n,profile:e.profile}):Ne(i,{timeoutSeconds:n,profile:e.profile})),We=new Map(b.map((i)=>[i.site,i])),p=new Map(v),N=new Map,P=new Map;for(const i of ce){p.set(i.site,i);if(r==="full"){P.set(i.site,f);if(i.status!=="error")N.set(i.site,f)}}const B=(i)=>{const c=N.get(i.site);if(c!==void 0)return c;const y=m[i.site];return y&&y.status===i.status&&X(y.verifiedAt,f)?y.verifiedAt:void 0},le=(i)=>P.get(i.site)??m[i.site]?.verifyAttemptAt,F={...m};for(const i of p.keys()){if(v.has(i))continue;const c=p.get(i);if(!c)continue;F[i]=Ae(c,f,B(c),le(c))}A.profiles[l]=F;await Se(d,A).catch(()=>{});const j=b.map((i)=>p.get(i.site)).filter((i)=>i!==void 0).map((i)=>({...i,checked_at:i.checked_at??new Date(f).toISOString(),verified:i.verified??B(i)!==void 0})),U=s.replace(/-/g,"_");return U==="all"?j:j.filter((i)=>i.status===U)}export async function collectAuthRefresh(e){const t=Y(e.sites),r=k(e.concurrency,"--concurrency",3),o=k(e.timeout,"--timeout",20),n=e.statePath??ye(),s=e.now??new Date,a=await Re(n),u=ee().filter((l)=>!t||t.has(l.site)),d=await se(u,e.profile),f=await ae(u,r,(l)=>d&&l.browser===!0&&!E(l.site)?Promise.resolve({site:l.site,status:"error",last_touched_at:a.sites[l.site]?.last_touched_at??"",next_refresh_at:h(a.sites[l.site]),error:`BROWSER_CONNECT: ${d}`}):Be(l,{timeoutSeconds:o,profile:e.profile,now:s,state:a,force:e.all===!0}));await Ee(n,a);return f}const ue=["logged_in","error","unknown","not_logged_in"];function je(e){const t=[e.identity,e.verified?"verified":""].filter(Boolean);return t.length>0?`${e.site}(${t.join(", ")})`:e.site}export function summarizeAuthStatus(e){const t=new Map;for(const r of e){const o=r.status==="error"||r.status==="unknown"?r.error:"",n=`${r.status}\x00${o}`,s=t.get(n)??{status:r.status,note:o,rows:[]};s.rows.push(r);t.set(n,s)}return[...t.values()].sort((r,o)=>ue.indexOf(r.status)-ue.indexOf(o.status)).map((r)=>({status:r.status,count:r.rows.length,sites:r.rows.map(je).join(", "),note:r.note,checked_at:r.rows.map((o)=>o.checked_at??"").filter(Boolean).sort()[0]??""}))}function Ue(){return process.env.PUBLISHPORT_REMOTE==="1"}export function registerAuthCommands(e){const t=e.command("auth").description("Inspect website login status"),r=t.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").option("--max-age <seconds>","Reuse a local cached verdict younger than this instead of probing").option("--verify <n>","Deprecated and ignored: batch auth status never navigates. Use --site <s> --full to confirm one site with a real whoami").addOption(new he("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("--rows","One row per site with every field. Remote PublishPort sessions without --site default to one row per status (sites listed by name)",!1).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(n)=>{const s=typeof r.optsWithGlobals==="function"?r.optsWithGlobals():{},a=await collectAuthStatus({sites:n.site,full:n.full===!0,concurrency:n.concurrency,timeout:n.timeout,only:n.only,maxAge:n.maxAge,verify:n.verify,profile:typeof s.profile==="string"&&s.profile.trim()?s.profile.trim():void 0}),u=typeof n.format==="string"?n.format:"table";if(Ue()&&!n.site&&n.rows!==!0){process.stderr.write(`auth status: 按状态汇总(远程会话默认);逐站全字段加 --rows,单站明细加 --site <站点>
4
+ `);O(summarizeAuthStatus(a),{fmt:u,fmtExplicit:r.getOptionValueSource("format")==="cli",columns:["status","count","sites","note","checked_at"],title:"ppcli/auth status",source:"quick auth check, summarized by status"});return}O(a,{fmt:u,fmtExplicit:r.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"ppcli/auth status",source:n.full?"full whoami probe":"quick auth check"})}),o=t.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(n)=>{const s=typeof o.optsWithGlobals==="function"?o.optsWithGlobals():{},a=await collectAuthRefresh({sites:n.site,all:n.all===!0,concurrency:n.concurrency,timeout:n.timeout,profile:typeof s.profile==="string"&&s.profile.trim()?s.profile.trim():void 0}),u=typeof n.format==="string"?n.format:"table";O(a,{fmt:u,fmtExplicit:o.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"ppcli/auth refresh",source:n.all?"forced persistent touch":"persistent touch with 24h throttle"})});return t}
@@ -1,2 +1,8 @@
1
- import g from"cli-table3";import m from"js-yaml";function i(e){if(Array.isArray(e))return e;if(e&&typeof e==="object")return[e];return[{value:e}]}function f(e,r){return r.columns??Object.keys(e[0]??{})}export function render(e,r={}){let o=r.fmt??"table";if(!r.fmtExplicit){if(o==="table"&&!process.stdout.isTTY)o="yaml"}if(e===null||e===void 0){console.log(e);return}switch(o){case"json":h(e);break;case"plain":p(e,r);break;case"md":case"markdown":b(e,r);break;case"csv":w(e,r);break;case"yaml":case"yml":y(e);break;default:d(e,r);break}}function d(e,r){const o=i(e);if(!o.length){console.log("(no data)");return}const l=f(o,r),s=l.map((c)=>k(c)),t=new g({head:s.map((c)=>c),style:{head:[],border:[]},wordWrap:!0,wrapOnWordBoundary:!0});for(const c of o)t.push(l.map((a)=>{const u=c[a];return u===null||u===void 0?"":String(u)}));console.log();if(r.title)console.log(` ${r.title}`);console.log(t.toString());const n=[];n.push(`${o.length} items`);if(r.elapsed!==void 0)n.push(`${r.elapsed.toFixed(1)}s`);if(r.source)n.push(r.source);if(r.footerExtra)n.push(r.footerExtra);console.log(n.join(" · "))}function h(e){console.log(JSON.stringify(e,null,2))}function p(e,r){const o=i(e);if(!o.length)return;if(o.length===1){const l=o[0],s=Object.entries(l);if(s.length===1){const[t,n]=s[0];if(t==="response"||t==="content"||t==="markdown"||t==="text"||t==="value"){console.log(String(n??""));return}}}o.forEach((l,s)=>{Object.entries(l).filter(([,n])=>n!==void 0&&n!==null&&String(n)!=="").forEach(([n,c])=>{console.log(`${n}: ${c}`)});if(s<o.length-1)console.log("")})}function b(e,r){const o=i(e);if(!o.length)return;if(o.length===1){const t=Object.entries(o[0]);if(t.length===1){const[n,c]=t[0];if(n==="content"||n==="markdown"||n==="text"||n==="value"){console.log(String(c??""));return}}}const l=f(o,r),s=(t)=>t.replace(/\|/g,"\\|").replace(/\r\n|\r|\n/g,"<br>");console.log("| "+l.map(s).join(" | ")+" |");console.log("| "+l.map(()=>"---").join(" | ")+" |");for(const t of o)console.log("| "+l.map((n)=>s(String(t[n]??""))).join(" | ")+" |")}function w(e,r){const o=i(e);if(!o.length)return;const l=f(o,r);console.log(l.join(","));for(const s of o)console.log(l.map((t)=>{const n=String(s[t]??"");return n.includes(",")||n.includes('"')||n.includes(`
2
- `)||n.includes("\r")?`"${n.replace(/"/g,'""')}"`:n}).join(","))}function y(e){console.log(m.dump(e,{sortKeys:!1,lineWidth:120,noRefs:!0}))}function k(e){return e.charAt(0).toUpperCase()+e.slice(1)}
1
+ import p from"cli-table3";import g from"js-yaml";function c(n){if(Array.isArray(n))return n;if(n&&typeof n==="object")return[n];return[{value:n}]}function u(n,o){return o.columns??Object.keys(n[0]??{})}export function render(n,o={}){let e=o.fmt??"table";if(!o.fmtExplicit){if(e==="table"&&!process.stdout.isTTY)e="yaml"}if(n===null||n===void 0){console.log(n);return}switch(e){case"json":d(n);break;case"plain":b(n,o);break;case"md":case"markdown":w(n,o);break;case"csv":j(n,o);break;case"yaml":case"yml":k(n);break;default:y(n,o);break}}function y(n,o){const e=c(n);if(!e.length){console.log("(no data)");return}const l=u(e,o),s=l.map((i)=>O(i)),t=new p({head:s.map((i)=>i),style:{head:[],border:[]},wordWrap:!0,wrapOnWordBoundary:!0});for(const i of e)t.push(l.map((a)=>{const f=i[a];return f===null||f===void 0?"":String(f)}));console.log();if(o.title)console.log(` ${o.title}`);console.log(t.toString());const r=[];r.push(`${e.length} items`);if(o.elapsed!==void 0)r.push(`${o.elapsed.toFixed(1)}s`);if(o.source)r.push(o.source);if(o.footerExtra)r.push(o.footerExtra);console.log(r.join(" · "))}function m(){return!process.stdout.isTTY}function h(n){return n!==null&&typeof n==="object"&&!Array.isArray(n)}function d(n){if(!m()){console.log(JSON.stringify(n,null,2));return}if(Array.isArray(n)){console.log(n.length===0?"[]":`[
2
+ ${n.map((o)=>JSON.stringify(o)??"null").join(`,
3
+ `)}
4
+ ]`);return}if(h(n)&&typeof n.toJSON!=="function"){const o=Object.entries(n).filter(([,e])=>e!==void 0&&typeof e!=="function");console.log(o.length===0?"{}":`{
5
+ ${o.map(([e,l])=>`${JSON.stringify(e)}:${JSON.stringify(l)}`).join(`,
6
+ `)}
7
+ }`);return}console.log(JSON.stringify(n))}function b(n,o){const e=c(n);if(!e.length)return;if(e.length===1){const l=e[0],s=Object.entries(l);if(s.length===1){const[t,r]=s[0];if(t==="response"||t==="content"||t==="markdown"||t==="text"||t==="value"){console.log(String(r??""));return}}}e.forEach((l,s)=>{Object.entries(l).filter(([,r])=>r!==void 0&&r!==null&&String(r)!=="").forEach(([r,i])=>{console.log(`${r}: ${i}`)});if(s<e.length-1)console.log("")})}function w(n,o){const e=c(n);if(!e.length)return;if(e.length===1){const t=Object.entries(e[0]);if(t.length===1){const[r,i]=t[0];if(r==="content"||r==="markdown"||r==="text"||r==="value"){console.log(String(i??""));return}}}const l=u(e,o),s=(t)=>t.replace(/\|/g,"\\|").replace(/\r\n|\r|\n/g,"<br>");console.log("| "+l.map(s).join(" | ")+" |");console.log("| "+l.map(()=>"---").join(" | ")+" |");for(const t of e)console.log("| "+l.map((r)=>s(String(t[r]??""))).join(" | ")+" |")}function j(n,o){const e=c(n);if(!e.length)return;const l=u(e,o);console.log(l.join(","));for(const s of e)console.log(l.map((t)=>{const r=String(s[t]??"");return r.includes(",")||r.includes('"')||r.includes(`
8
+ `)||r.includes("\r")?`"${r.replace(/"/g,'""')}"`:r}).join(","))}function k(n){console.log(m()?g.dump(n,{sortKeys:!1,lineWidth:-1,noRefs:!0,flowLevel:1}):g.dump(n,{sortKeys:!1,lineWidth:120,noRefs:!0}))}function O(n){return n.charAt(0).toUpperCase()+n.slice(1)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.0.48",
3
+ "version": "1.0.49",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false