publishport-opencli 1.0.47 → 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}:{}}})}});
@@ -6,6 +6,8 @@ export interface DaemonStatus {
6
6
  extensionConnected: boolean;
7
7
  extensionVersion?: string;
8
8
  extensionCompatRange?: string;
9
+ /** Shell primitives advertised by the extension (1.1.5+: chrome-api, cdp-raw, events, bind-tab). */
10
+ extensionCapabilities?: string[];
9
11
  contextId?: string;
10
12
  profileRequired?: boolean;
11
13
  profileDisconnected?: boolean;
@@ -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 +1 @@
1
- import{fetchDaemonStatus as a,requestDaemonShutdown as u}from"../browser/daemon-transport.js";import{restartDaemon as f}from"../browser/daemon-lifecycle.js";import{formatDuration as m}from"../download/progress.js";import{log as o}from"../logger.js";import{PKG_VERSION as c}from"../version.js";import{formatDaemonVersion as l,isDaemonStale as d}from"../browser/daemon-version.js";import{browserSession as w,getBrowserFactory as x}from"../runtime.js";export async function daemonStatus(s={}){const e=await a();if(s.json){console.log(JSON.stringify(e?{daemon:"running",pid:e.pid,daemonVersion:l(e),stale:d(e,c),uptimeSeconds:Math.round(e.uptime),bridgeReady:!!e.extensionConnected,extensionConnected:!!e.extensionConnected,extensionVersion:e.extensionVersion??null,profileRequired:!!e.profileRequired,profileDisconnected:!!e.profileDisconnected,profiles:(e.profiles??[]).map((t)=>({contextId:t.contextId,extensionVersion:t.extensionVersion??null})),port:e.port}:{daemon:"not_running",bridgeReady:!1},null,2));return}if(!e){console.log("Daemon: not running");return}let r;if(e.extensionConnected)r=e.extensionVersion?`connected (v${e.extensionVersion})`:"connected (version unknown)";else if(e.profileRequired){const t=e.profiles?.length??0;r=`${t} ${t===1?"profile":"profiles"} connected, none selected — run \`ppcli profile use <name>\``}else if(e.profileDisconnected)r="requested profile not connected — run `ppcli profile use <name>`";else r="disconnected";const i=l(e),n=d(e,c);console.log(`Daemon: ${n?"stale":"running"} (PID ${e.pid})`);console.log(`Version: ${i}${n?` (CLI v${c}; run: ppcli daemon restart)`:""}`);console.log(`Uptime: ${m(Math.round(e.uptime*1000))}`);console.log(`Extension: ${r}`);if(e.profiles&&e.profiles.length>0)console.log(`Profiles: ${e.profiles.map((t)=>{const p=t.extensionVersion?` v${t.extensionVersion}`:"";return`${t.contextId}${p}`}).join(", ")}`);console.log(`Memory: ${e.memoryMB} MB`);console.log(`Port: ${e.port}`)}export async function daemonStop(){if(!await a()){o.info("Daemon is not running.");return}if(await u())o.success("Daemon stopped.");else{o.error("Failed to stop daemon.");process.exitCode=1}}export async function daemonRestart(){const s=await a();if(s?.profiles&&s.profiles.length>0)o.warn(`Restarting daemon will disconnect ${s.profiles.length} browser profile(s); the extension should reconnect automatically.`);const e=await f();if(!e.stopped){o.error("Failed to stop daemon before restart.");process.exitCode=1;return}if(!e.status){o.error("Daemon restart timed out before the new daemon reported status.");process.exitCode=1;return}const r=e.previousStatus?"restarted":"started",i=l(e.status);o.success(`Daemon ${r} on port ${e.status.port} (${i}).`);if(e.status.extensionConnected){const n=e.status.profiles?.length??0,t=n>0?`; profiles connected: ${n}`:"";o.status(`Extension connected${t}.`)}else o.warn("Daemon is running, but the Browser Bridge extension has not connected yet.")}export async function daemonWarm(s){const e=await a();if(!e){o.warn("Daemon/extension not ready; skip warm (retry after the browser extension connects).");return}if(!e.extensionConnected){o.warn("Browser extension not connected yet; skip warm.");return}const r=s&&/^https?:\/\//i.test(s)?s:"about:blank",i=x("__warm__");try{await w(i,async(n)=>{await n.goto(r).catch(()=>{})},{session:"site:__warm__",windowMode:"background",surface:"adapter",siteSession:"persistent"});o.success(`Automation window warmed (${r}, kept alive for reuse).`)}catch(n){o.warn(`Warm failed (non-fatal): ${n instanceof Error?n.message:String(n)}`)}}
1
+ import{fetchDaemonStatus as a,requestDaemonShutdown as u}from"../browser/daemon-transport.js";import{restartDaemon as m}from"../browser/daemon-lifecycle.js";import{formatDuration as f}from"../download/progress.js";import{log as o}from"../logger.js";import{PKG_VERSION as c}from"../version.js";import{formatDaemonVersion as l,isDaemonStale as d}from"../browser/daemon-version.js";import{browserSession as w,getBrowserFactory as x}from"../runtime.js";export async function daemonStatus(s={}){const e=await a();if(s.json){console.log(JSON.stringify(e?{daemon:"running",pid:e.pid,daemonVersion:l(e),stale:d(e,c),uptimeSeconds:Math.round(e.uptime),bridgeReady:!!e.extensionConnected,extensionConnected:!!e.extensionConnected,extensionVersion:e.extensionVersion??null,profileRequired:!!e.profileRequired,profileDisconnected:!!e.profileDisconnected,profiles:(e.profiles??[]).map((t)=>({contextId:t.contextId,extensionVersion:t.extensionVersion??null})),port:e.port}:{daemon:"not_running",bridgeReady:!1},null,2));return}if(!e){console.log("Daemon: not running");return}let r;if(e.extensionConnected)r=e.extensionVersion?`connected (v${e.extensionVersion})`:"connected (version unknown)";else if(e.profileRequired){const t=e.profiles?.length??0;r=`${t} ${t===1?"profile":"profiles"} connected, none selected — run \`ppcli profile use <name>\``}else if(e.profileDisconnected)r="requested profile not connected — run `ppcli profile use <name>`";else r="disconnected";const i=l(e),n=d(e,c);console.log(`Daemon: ${n?"stale":"running"} (PID ${e.pid})`);console.log(`Version: ${i}${n?` (CLI v${c}; run: ppcli daemon restart)`:""}`);console.log(`Uptime: ${f(Math.round(e.uptime*1000))}`);console.log(`Extension: ${r}`);if(e.profiles&&e.profiles.length>0)console.log(`Profiles: ${e.profiles.map((t)=>{const p=t.extensionVersion?` v${t.extensionVersion}`:"";return`${t.contextId}${p}`}).join(", ")}`);console.log(`Memory: ${e.memoryMB} MB`);console.log(`Port: ${e.port}`)}export async function daemonStop(){if(!await a()){o.info("Daemon is not running.");return}if(await u())o.success("Daemon stopped.");else{o.error("Failed to stop daemon.");process.exitCode=1}}export async function daemonRestart(){const s=await a();if(s?.profiles&&s.profiles.length>0)o.warn(`Restarting daemon will disconnect ${s.profiles.length} browser profile(s); the extension should reconnect automatically.`);const e=await m();if(!e.stopped){o.error("Failed to stop daemon before restart.");process.exitCode=1;return}if(!e.status){o.error("Daemon restart timed out before the new daemon reported status.");process.exitCode=1;return}const r=e.previousStatus?"restarted":"started",i=l(e.status);o.success(`Daemon ${r} on port ${e.status.port} (${i}).`);if(e.status.extensionConnected){const n=e.status.profiles?.length??0,t=n>0?`; profiles connected: ${n}`:"";o.status(`Extension connected${t}.`)}else o.warn("Daemon is running, but the Browser Bridge extension has not connected yet.")}export async function daemonWarm(s){const e=await a();if(!e){o.warn("Daemon/extension not ready; skip warm (retry after the browser extension connects).");return}if(!e.extensionConnected){o.warn("Browser extension not connected yet; skip warm.");return}if(e.extensionCapabilities?.includes("chrome-api")){o.status("Extension hosts background tabs in existing windows; nothing to warm.");return}const r=s&&/^https?:\/\//i.test(s)?s:"about:blank",i=x("__warm__");try{await w(i,async(n)=>{await n.goto(r).catch(()=>{})},{session:"site:__warm__",windowMode:"background",surface:"adapter",siteSession:"persistent"});o.success(`Automation window warmed (${r}, kept alive for reuse).`)}catch(n){o.warn(`Warm failed (non-fatal): ${n instanceof Error?n.message:String(n)}`)}}
@@ -1 +1 @@
1
- import{createServer as B}from"node:http";import{WebSocketServer as Y,WebSocket as V}from"ws";import{DEFAULT_DAEMON_PORT as Z,PUBLISHPORT_DAEMON_PORT as Q,unsupportedDaemonPortEnvMessage as q}from"./constants.js";import{EXIT_CODES as v}from"./errors.js";import{log as a}from"./logger.js";import{PKG_VERSION as ee}from"./version.js";import{DEFAULT_CONTEXT_ID as ne}from"./browser/profile.js";import{buildCommandDispatchFailure as G,buildCommandTimeoutFailure as te,buildExtensionDisconnectFailure as oe,getResponseCorsHeaders as re,resolveProfileRoute as ie}from"./daemon-utils.js";import*as C from"./browser/trace.js";import{DebuggerBanner as se,supportsDebuggerBanner as ae}from"./browser/debugger-banner.js";import{SessionLeaseRegistry as ce,buildSessionBusyFailure as de,getSessionLeaseKey as le,isSessionLeaseCommand as ue}from"./session-lease.js";const S=Z,D=Q;if(process.env.OPENCLI_DAEMON_PORT){a.error(q(process.env.OPENCLI_DAEMON_PORT));process.exit(v.USAGE_ERROR)}const x=new Map,u=new Map,_=new ce;function F(n){for(const t of u.values())if(t.runId===n)return!0;return!1}function w(n,t,s){clearTimeout(t.timer);u.delete(n);if(t.leaseKey&&t.runId)_.heartbeat(t.leaseKey,t.runId,Date.now());for(const r of t.settlers)if(s.error)r.reject(s.error);else r.resolve(s.data)}let fe=0;function me(n,t,s=15000){const r=`daemon-${process.pid}-${++fe}`;return new Promise((o,e)=>{const i=setTimeout(()=>{const d=u.get(r);if(d)w(r,d,{error:Error(`daemon ${String(t.action)} timed out after ${s}ms`)})},s),c={contextId:n.contextId,action:String(t.action),dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:(d)=>o(d),reject:e}],timer:i};u.set(r,c);try{n.ws.send(JSON.stringify({...t,id:r}),(d)=>{if(d&&u.get(r)===c)w(r,c,{error:d})});c.dispatched=!0}catch(d){w(r,c,{error:d instanceof Error?d:Error(String(d))})}})}let A=0;const pe=200,P=[];class b extends Error{errorCode;errorHint;status;constructor(n,t,s,r=400){super(n);this.errorCode=t;this.errorHint=s;this.status=r;this.name="DaemonCommandFailure"}}function ge(n){P.push(n);if(P.length>pe)P.shift()}function W(){return[...x.values()].filter((n)=>n.ws.readyState===V.OPEN)}function J(n,t){const s=W(),r=ie(s.map((e)=>e.contextId),n,t);if(r.kind==="error"){if(r.errorCode==="profile_disconnected"&&t==="write"&&s.length===1)a.warn(`[daemon] Refusing to route a WRITE to fallback profile "${s[0].contextId}" (requested "${n}")`);return{errorCode:r.errorCode,error:r.error,errorHint:r.errorHint}}if(r.kind==="fallback")a.warn(`[daemon] Requested profile "${r.requestedContextId}" not connected; falling back to the only connected profile "${r.contextId}"`);const o=s.find((e)=>e.contextId===r.contextId);if(!o)return{errorCode:"extension_not_connected",error:"浏览器桥未连接:这台机器上的 PublishPort 托管浏览器没在运行。",errorHint:"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」把托管浏览器拉起来;仍不行就重启客户端。"};return{connection:o}}function he(n,t){const s=typeof t==="string"&&t.trim()?t.trim():ne,r=x.get(s);if(r&&r.ws!==n)r.ws.close();const o=[...x.entries()].find(([,c])=>c.ws===n);if(o&&o[0]!==s)x.delete(o[0]);const e=x.get(s),i={contextId:s,ws:n,extensionVersion:e?.ws===n?e.extensionVersion:null,extensionCompatRange:e?.ws===n?e.extensionCompatRange:null,lastSeenAt:Date.now(),capabilities:e?.ws===n?e.capabilities:[],banner:e?.ws===n?e.banner:null};x.set(s,i);return i}function j(n){for(const[t,s]of x.entries()){if(s.ws!==n)continue;s.banner?.dispose();x.delete(t);C.traceExtDisconnect(t);for(const[r,o]of u){if(o.contextId!==t)continue;const e=oe({contextId:t,action:o.action,dispatched:o.dispatched});if(e.countAsCommandResultUnknown){A++;a.warn(`[daemon] Command result unknown after extension disconnect (id=${r}, action=${o.action}, context=${t})`)}C.traceResult(r,!1,Date.now()-o.dispatchTs,e.errorCode);w(r,o,{error:new b(e.message,e.errorCode,e.errorHint,e.status)})}}}const K=33554432;class L extends Error{status=413;constructor(){super(`请求体超过 ${K/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function xe(n){return new Promise((t,s)=>{const r=[];let o=0,e=!1;n.on("data",(i)=>{if(e)return;o+=i.length;if(o>K){e=!0;r.length=0;s(new L);return}r.push(i)});n.on("end",()=>{if(!e)t(Buffer.concat(r).toString("utf-8"))});n.on("error",(i)=>{if(!e)s(i)})})}function l(n,t,s,r){n.writeHead(t,{"Content-Type":"application/json",...r});n.end(JSON.stringify(s))}async function X(n,t){const s=n.headers.origin;if(s&&!s.startsWith("chrome-extension://")){l(t,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(n.method==="OPTIONS"){t.writeHead(204);t.end();return}const r=n.url??"/",o=r.split("?")[0];if(n.method==="GET"&&o==="/ping"){l(t,200,{ok:!0},re(o,s));return}if(!n.headers["x-opencli"]){l(t,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(n.method==="GET"&&o==="/status"){const e=process.uptime(),i=process.memoryUsage(),d=new URL(r,`http://localhost:${S}`).searchParams.get("contextId")?.trim()||void 0,m=J(d),k=W().map((y)=>({contextId:y.contextId,extensionConnected:!0,extensionVersion:y.extensionVersion??void 0,extensionCompatRange:y.extensionCompatRange??void 0,pending:[...u.values()].filter((g)=>g.contextId===y.contextId).length,lastSeenAt:y.lastSeenAt}));l(t,200,{ok:!0,pid:process.pid,uptime:e,daemonVersion:ee,extensionConnected:!!m.connection,extensionVersion:m.connection?.extensionVersion??void 0,extensionCompatRange:m.connection?.extensionCompatRange??void 0,contextId:m.connection?.contextId??d,profileRequired:m.errorCode==="profile_required",profileDisconnected:m.errorCode==="profile_disconnected",profiles:k,pending:u.size,sessionLeases:_.list(Date.now(),F),commandResultUnknown:A,memoryMB:Math.round(i.rss/1024/1024*10)/10,port:S});return}if(n.method==="GET"&&o==="/logs"){const i=new URL(r,`http://localhost:${S}`).searchParams.get("level"),c=i?P.filter((d)=>d.level===i):P;l(t,200,{ok:!0,logs:c});return}if(n.method==="DELETE"&&o==="/logs"){P.length=0;l(t,200,{ok:!0});return}if(n.method==="POST"&&o==="/shutdown"){l(t,200,{ok:!0,message:"Shutting down"});setTimeout(()=>N(),100);return}if(n.method==="POST"&&r==="/command"){try{const e=JSON.parse(await xe(n));if(!e.id){l(t,400,{ok:!1,error:"Missing command id"});return}if(e.action==="lease-release"){if(typeof e.runId==="string")_.releaseByRunId(e.runId);l(t,200,{id:e.id,ok:!0});return}const i=J(typeof e.contextId==="string"?e.contextId:void 0,e.access);if(!i.connection){l(t,i.errorCode==="profile_required"?409:503,{id:e.id,ok:!1,errorCode:i.errorCode,error:i.error,...i.errorHint?{errorHint:i.errorHint}:{}});return}let c,d;if(ue(e)){const R=Date.now(),E=le(i.connection.contextId,e.surface,e.session),I=_.touch(E,{runId:e.runId,command:typeof e.command==="string"&&e.command?e.command:e.action,now:R,hasPendingWork:F});if(!I.granted){const p=de(e.session,I.holder,R);a.warn(`[daemon] Session ${E} busy — rejected ${e.command??e.action} (runId=${e.runId}); held by ${I.holder.command} (runId=${I.holder.runId})`);l(t,p.status,{id:e.id,ok:!1,errorCode:p.errorCode,error:p.message,errorHint:p.errorHint});return}c=E;d=e.runId}const m=typeof e.deadlineAt==="number"&&e.deadlineAt>0?Math.max(1000,e.deadlineAt-Date.now()):typeof e.timeout==="number"&&e.timeout>0?e.timeout*1000:120000,k=u.get(e.id);if(k){const R=await new Promise((E,I)=>{k.settlers.push({resolve:E,reject:I})});l(t,200,R);return}const y=i.connection.contextId;i.connection.banner?.onActivity();const g=await new Promise((R,E)=>{const I=setTimeout(()=>{const f=u.get(e.id);if(!f)return;C.traceTimeout(e.id,m,typeof e.action==="string"?e.action:void 0);const h=te(f.action,m);if(h.countAsCommandResultUnknown&&f.dispatched){A++;a.warn(`[daemon] Command timed out after dispatch (id=${e.id}, action=${f.action}, timeout=${m}ms)`)}w(e.id,f,{error:new b(h.message,h.errorCode,h.errorHint,h.status)})},m),p={contextId:i.connection.contextId,action:typeof e.action==="string"?e.action:"unknown",dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:R,reject:E}],timer:I,...c&&d?{leaseKey:c,runId:d}:{}};u.set(e.id,p);const M=(f)=>{if(u.get(e.id)!==p)return;const h=G(p.contextId);w(e.id,p,{error:new b(h.message,h.errorCode,h.errorHint,h.status)});a.warn(`[daemon] Failed to dispatch command ${e.id}: ${f instanceof Error?f.message:String(f)}`)};try{i.connection.ws.send(JSON.stringify(e),(f)=>{if(f&&!p.dispatched)M(f)});p.dispatched=!0;C.traceDispatch(e)}catch(f){M(f)}}),U=g&&typeof g==="object"?g.page:void 0;i.connection.banner?.onActivity(typeof U==="string"?U:void 0);l(t,200,g&&typeof g==="object"&&!Array.isArray(g)?{...g,routedContextId:y}:g)}catch(e){const i=e instanceof b?e:null;if(e instanceof L){l(t,413,{ok:!1,error:e.message,errorCode:"body_too_large"});return}l(t,i?.status??(e instanceof Error&&e.message.includes("timeout")?408:400),{ok:!1,error:e instanceof Error?e.message:"Invalid request",...i?.errorCode?{errorCode:i.errorCode}:{},...i?.errorHint?{errorHint:i.errorHint}:{}})}return}l(t,404,{error:"Not found"})}const T=B((n,t)=>{X(n,t).catch(()=>{t.writeHead(500);t.end()})}),O=B((n,t)=>{X(n,t).catch(()=>{t.writeHead(500);t.end()})}),H=new Y({noServer:!0});function z(n,t,s){if((n.url??"/").split("?")[0]!=="/ext"){t.destroy();return}const o=n.headers.origin;if(o&&!o.startsWith("chrome-extension://")){t.destroy();return}H.handleUpgrade(n,t,s,(e)=>{H.emit("connection",e,n)})}T.on("upgrade",z);O.on("upgrade",z);H.on("connection",(n)=>{a.info("[daemon] Extension connected");let t=0;const s=setInterval(()=>{if(n.readyState!==V.OPEN){clearInterval(s);return}if(t>=2){a.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(s);n.terminate();return}t++;n.ping()},15000);n.on("pong",()=>{t=0});n.on("message",(r)=>{try{const o=JSON.parse(r.toString());if(o.type==="hello"){if(o.brand!=="publishport"){a.warn(`[daemon] 拒绝非 PublishPort 官方扩展连接(brand=${JSON.stringify(o.brand)})`);n.close(4003,"Only the official PublishPort extension is supported");return}const i=he(n,o.contextId);i.extensionVersion=typeof o.version==="string"?o.version:null;i.extensionCompatRange=typeof o.compatRange==="string"?o.compatRange:null;i.capabilities=Array.isArray(o.capabilities)?o.capabilities.filter((c)=>typeof c==="string"):[];i.lastSeenAt=Date.now();i.banner?.dispose();i.banner=ae(i.capabilities)?new se((c)=>me(i,c),(c)=>a.warn(c)):null;i.banner?.start();a.info(`[daemon] Extension profile connected: ${i.contextId}`);C.traceExtConnect(i.contextId,i.extensionVersion);return}if(o.type==="log"){if(o.level==="error")a.error(`[ext] ${o.msg}`);else if(o.level==="warn")a.warn(`[ext] ${o.msg}`);else a.info(`[ext] ${o.msg}`);ge({level:o.level,msg:o.msg,ts:o.ts??Date.now()});C.traceExtLog(o.level,o.msg);return}if(o.type==="ping")return;if(o.type==="event"){for(const i of x.values())if(i.ws===n)i.banner?.onEvent(String(o.name),o.args);return}const e=u.get(o.id);if(e){C.traceResult(o.id,!!o.ok,Date.now()-e.dispatchTs,typeof o.errorCode==="string"?o.errorCode:void 0,typeof o.page==="string"?o.page:void 0);w(o.id,e,{data:o})}}catch(o){const e=r.toString().slice(0,200);a.warn(`[daemon] Ignoring malformed WS message from extension: ${o instanceof Error?o.message:String(o)} (first 200 chars: ${JSON.stringify(e)})`)}});n.on("close",()=>{a.info("[daemon] Extension disconnected");clearInterval(s);j(n)});n.on("error",()=>{clearInterval(s);j(n)})});T.listen(S,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${S}`)});T.on("error",(n)=>{if(n.code==="EADDRINUSE"){a.error(`[daemon] Port ${S} already in use — another daemon is likely running. Exiting.`);process.exit(v.SERVICE_UNAVAIL)}a.error(`[daemon] Server error: ${n.message}`);process.exit(v.GENERIC_ERROR)});O.listen(D,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${D} (PublishPort extension)`)});O.on("error",(n)=>{a.warn(`[daemon] PublishPort port ${D} unavailable (${n.message}) — extension will fall back to ${S}`)});function N(){for(const[n,t]of u){const s=t.dispatched?new b("Daemon shutting down before the command completed.","daemon_shutting_down","The daemon is being replaced; a journaling extension replays the command result on retry.",503):(()=>{const r=G(t.contextId);return new b(r.message,r.errorCode,r.errorHint,r.status)})();w(n,t,{error:s})}u.clear();for(const n of x.values()){n.banner?.shutdown();n.ws.close()}O.close();T.close(()=>process.exit(v.SUCCESS));setTimeout(()=>{T.closeIdleConnections?.();O.closeIdleConnections?.();setTimeout(()=>process.exit(v.SUCCESS),500).unref()},100).unref()}process.on("SIGTERM",N);process.on("SIGINT",N);
1
+ import{createServer as V}from"node:http";import{WebSocketServer as Q,WebSocket as G}from"ws";import{DEFAULT_DAEMON_PORT as q,PUBLISHPORT_DAEMON_PORT as ee,unsupportedDaemonPortEnvMessage as ne}from"./constants.js";import{EXIT_CODES as P}from"./errors.js";import{log as a}from"./logger.js";import{PKG_VERSION as te}from"./version.js";import{DEFAULT_CONTEXT_ID as oe}from"./browser/profile.js";import{buildCommandDispatchFailure as W,buildCommandTimeoutFailure as re,buildExtensionDisconnectFailure as ie,getResponseCorsHeaders as se,resolveProfileRoute as ae}from"./daemon-utils.js";import*as E from"./browser/trace.js";import{DebuggerBanner as ce,supportsDebuggerBanner as F}from"./browser/debugger-banner.js";import{SessionLeaseRegistry as de,buildSessionBusyFailure as le,getSessionLeaseKey as ue,isSessionLeaseCommand as fe}from"./session-lease.js";const C=q,A=ee;if(process.env.OPENCLI_DAEMON_PORT){a.error(ne(process.env.OPENCLI_DAEMON_PORT));process.exit(P.USAGE_ERROR)}const x=new Map,u=new Map,_=new de;function J(n){for(const t of u.values())if(t.runId===n)return!0;return!1}function I(n,t,s){clearTimeout(t.timer);u.delete(n);if(t.leaseKey&&t.runId)_.heartbeat(t.leaseKey,t.runId,Date.now());for(const r of t.settlers)if(s.error)r.reject(s.error);else r.resolve(s.data)}let me=0;function D(n,t,s=15000){const r=`daemon-${process.pid}-${++me}`;return new Promise((o,e)=>{const i=setTimeout(()=>{const d=u.get(r);if(d)I(r,d,{error:Error(`daemon ${String(t.action)} timed out after ${s}ms`)})},s),c={contextId:n.contextId,action:String(t.action),dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:(d)=>o(d),reject:e}],timer:i};u.set(r,c);try{n.ws.send(JSON.stringify({...t,id:r}),(d)=>{if(d&&u.get(r)===c)I(r,c,{error:d})});c.dispatched=!0}catch(d){I(r,c,{error:d instanceof Error?d:Error(String(d))})}})}const pe="https://publishport.app/automation*";async function ge(n){try{const t=await D(n,{action:"chrome-api",api:"tabs.query",args:[{url:pe}]});if(!t.ok)throw Error(t.error??"tabs.query failed");const s=(Array.isArray(t.data)?t.data:[]).map((o)=>o.id).filter((o)=>typeof o==="number");if(s.length===0)return;const r=await D(n,{action:"chrome-api",api:"tabs.remove",args:[s]});if(!r.ok)throw Error(r.error??"tabs.remove failed");a.info(`[daemon] Closed ${s.length} legacy automation info tab(s)`)}catch(t){a.warn(`[daemon] Closing legacy automation info tabs failed: ${t instanceof Error?t.message:String(t)}`)}}let H=0;const he=200,v=[];class S extends Error{errorCode;errorHint;status;constructor(n,t,s,r=400){super(n);this.errorCode=t;this.errorHint=s;this.status=r;this.name="DaemonCommandFailure"}}function xe(n){v.push(n);if(v.length>he)v.shift()}function j(){return[...x.values()].filter((n)=>n.ws.readyState===G.OPEN)}function K(n,t){const s=j(),r=ae(s.map((e)=>e.contextId),n,t);if(r.kind==="error"){if(r.errorCode==="profile_disconnected"&&t==="write"&&s.length===1)a.warn(`[daemon] Refusing to route a WRITE to fallback profile "${s[0].contextId}" (requested "${n}")`);return{errorCode:r.errorCode,error:r.error,errorHint:r.errorHint}}if(r.kind==="fallback")a.warn(`[daemon] Requested profile "${r.requestedContextId}" not connected; falling back to the only connected profile "${r.contextId}"`);const o=s.find((e)=>e.contextId===r.contextId);if(!o)return{errorCode:"extension_not_connected",error:"浏览器桥未连接:这台机器上的 PublishPort 托管浏览器没在运行。",errorHint:"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」把托管浏览器拉起来;仍不行就重启客户端。"};return{connection:o}}function ye(n,t){const s=typeof t==="string"&&t.trim()?t.trim():oe,r=x.get(s);if(r&&r.ws!==n)r.ws.close();const o=[...x.entries()].find(([,c])=>c.ws===n);if(o&&o[0]!==s)x.delete(o[0]);const e=x.get(s),i={contextId:s,ws:n,extensionVersion:e?.ws===n?e.extensionVersion:null,extensionCompatRange:e?.ws===n?e.extensionCompatRange:null,lastSeenAt:Date.now(),capabilities:e?.ws===n?e.capabilities:[],banner:e?.ws===n?e.banner:null};x.set(s,i);return i}function X(n){for(const[t,s]of x.entries()){if(s.ws!==n)continue;s.banner?.dispose();x.delete(t);E.traceExtDisconnect(t);for(const[r,o]of u){if(o.contextId!==t)continue;const e=ie({contextId:t,action:o.action,dispatched:o.dispatched});if(e.countAsCommandResultUnknown){H++;a.warn(`[daemon] Command result unknown after extension disconnect (id=${r}, action=${o.action}, context=${t})`)}E.traceResult(r,!1,Date.now()-o.dispatchTs,e.errorCode);I(r,o,{error:new S(e.message,e.errorCode,e.errorHint,e.status)})}}}const z=33554432;class U extends Error{status=413;constructor(){super(`请求体超过 ${z/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function Ie(n){return new Promise((t,s)=>{const r=[];let o=0,e=!1;n.on("data",(i)=>{if(e)return;o+=i.length;if(o>z){e=!0;r.length=0;s(new U);return}r.push(i)});n.on("end",()=>{if(!e)t(Buffer.concat(r).toString("utf-8"))});n.on("error",(i)=>{if(!e)s(i)})})}function l(n,t,s,r){n.writeHead(t,{"Content-Type":"application/json",...r});n.end(JSON.stringify(s))}async function Y(n,t){const s=n.headers.origin;if(s&&!s.startsWith("chrome-extension://")){l(t,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(n.method==="OPTIONS"){t.writeHead(204);t.end();return}const r=n.url??"/",o=r.split("?")[0];if(n.method==="GET"&&o==="/ping"){l(t,200,{ok:!0},se(o,s));return}if(!n.headers["x-opencli"]){l(t,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(n.method==="GET"&&o==="/status"){const e=process.uptime(),i=process.memoryUsage(),d=new URL(r,`http://localhost:${C}`).searchParams.get("contextId")?.trim()||void 0,m=K(d),k=j().map((w)=>({contextId:w.contextId,extensionConnected:!0,extensionVersion:w.extensionVersion??void 0,extensionCompatRange:w.extensionCompatRange??void 0,pending:[...u.values()].filter((g)=>g.contextId===w.contextId).length,lastSeenAt:w.lastSeenAt}));l(t,200,{ok:!0,pid:process.pid,uptime:e,daemonVersion:te,extensionConnected:!!m.connection,extensionVersion:m.connection?.extensionVersion??void 0,extensionCompatRange:m.connection?.extensionCompatRange??void 0,extensionCapabilities:m.connection?.capabilities??[],contextId:m.connection?.contextId??d,profileRequired:m.errorCode==="profile_required",profileDisconnected:m.errorCode==="profile_disconnected",profiles:k,pending:u.size,sessionLeases:_.list(Date.now(),J),commandResultUnknown:H,memoryMB:Math.round(i.rss/1024/1024*10)/10,port:C});return}if(n.method==="GET"&&o==="/logs"){const i=new URL(r,`http://localhost:${C}`).searchParams.get("level"),c=i?v.filter((d)=>d.level===i):v;l(t,200,{ok:!0,logs:c});return}if(n.method==="DELETE"&&o==="/logs"){v.length=0;l(t,200,{ok:!0});return}if(n.method==="POST"&&o==="/shutdown"){l(t,200,{ok:!0,message:"Shutting down"});setTimeout(()=>L(),100);return}if(n.method==="POST"&&r==="/command"){try{const e=JSON.parse(await Ie(n));if(!e.id){l(t,400,{ok:!1,error:"Missing command id"});return}if(e.action==="lease-release"){if(typeof e.runId==="string")_.releaseByRunId(e.runId);l(t,200,{id:e.id,ok:!0});return}const i=K(typeof e.contextId==="string"?e.contextId:void 0,e.access);if(!i.connection){l(t,i.errorCode==="profile_required"?409:503,{id:e.id,ok:!1,errorCode:i.errorCode,error:i.error,...i.errorHint?{errorHint:i.errorHint}:{}});return}let c,d;if(fe(e)){const R=Date.now(),b=ue(i.connection.contextId,e.surface,e.session),y=_.touch(b,{runId:e.runId,command:typeof e.command==="string"&&e.command?e.command:e.action,now:R,hasPendingWork:J});if(!y.granted){const p=le(e.session,y.holder,R);a.warn(`[daemon] Session ${b} busy — rejected ${e.command??e.action} (runId=${e.runId}); held by ${y.holder.command} (runId=${y.holder.runId})`);l(t,p.status,{id:e.id,ok:!1,errorCode:p.errorCode,error:p.message,errorHint:p.errorHint});return}c=b;d=e.runId}const m=typeof e.deadlineAt==="number"&&e.deadlineAt>0?Math.max(1000,e.deadlineAt-Date.now()):typeof e.timeout==="number"&&e.timeout>0?e.timeout*1000:120000,k=u.get(e.id);if(k){const R=await new Promise((b,y)=>{k.settlers.push({resolve:b,reject:y})});l(t,200,R);return}const w=i.connection.contextId;i.connection.banner?.onActivity();const g=await new Promise((R,b)=>{const y=setTimeout(()=>{const f=u.get(e.id);if(!f)return;E.traceTimeout(e.id,m,typeof e.action==="string"?e.action:void 0);const h=re(f.action,m);if(h.countAsCommandResultUnknown&&f.dispatched){H++;a.warn(`[daemon] Command timed out after dispatch (id=${e.id}, action=${f.action}, timeout=${m}ms)`)}I(e.id,f,{error:new S(h.message,h.errorCode,h.errorHint,h.status)})},m),p={contextId:i.connection.contextId,action:typeof e.action==="string"?e.action:"unknown",dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:R,reject:b}],timer:y,...c&&d?{leaseKey:c,runId:d}:{}};u.set(e.id,p);const B=(f)=>{if(u.get(e.id)!==p)return;const h=W(p.contextId);I(e.id,p,{error:new S(h.message,h.errorCode,h.errorHint,h.status)});a.warn(`[daemon] Failed to dispatch command ${e.id}: ${f instanceof Error?f.message:String(f)}`)};try{i.connection.ws.send(JSON.stringify(e),(f)=>{if(f&&!p.dispatched)B(f)});p.dispatched=!0;E.traceDispatch(e)}catch(f){B(f)}}),M=g&&typeof g==="object"?g.page:void 0;i.connection.banner?.onActivity(typeof M==="string"?M:void 0);l(t,200,g&&typeof g==="object"&&!Array.isArray(g)?{...g,routedContextId:w}:g)}catch(e){const i=e instanceof S?e:null;if(e instanceof U){l(t,413,{ok:!1,error:e.message,errorCode:"body_too_large"});return}l(t,i?.status??(e instanceof Error&&e.message.includes("timeout")?408:400),{ok:!1,error:e instanceof Error?e.message:"Invalid request",...i?.errorCode?{errorCode:i.errorCode}:{},...i?.errorHint?{errorHint:i.errorHint}:{}})}return}l(t,404,{error:"Not found"})}const T=V((n,t)=>{Y(n,t).catch(()=>{t.writeHead(500);t.end()})}),O=V((n,t)=>{Y(n,t).catch(()=>{t.writeHead(500);t.end()})}),N=new Q({noServer:!0});function Z(n,t,s){if((n.url??"/").split("?")[0]!=="/ext"){t.destroy();return}const o=n.headers.origin;if(o&&!o.startsWith("chrome-extension://")){t.destroy();return}N.handleUpgrade(n,t,s,(e)=>{N.emit("connection",e,n)})}T.on("upgrade",Z);O.on("upgrade",Z);N.on("connection",(n)=>{a.info("[daemon] Extension connected");let t=0;const s=setInterval(()=>{if(n.readyState!==G.OPEN){clearInterval(s);return}if(t>=2){a.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(s);n.terminate();return}t++;n.ping()},15000);n.on("pong",()=>{t=0});n.on("message",(r)=>{try{const o=JSON.parse(r.toString());if(o.type==="hello"){if(o.brand!=="publishport"){a.warn(`[daemon] 拒绝非 PublishPort 官方扩展连接(brand=${JSON.stringify(o.brand)})`);n.close(4003,"Only the official PublishPort extension is supported");return}const i=ye(n,o.contextId);i.extensionVersion=typeof o.version==="string"?o.version:null;i.extensionCompatRange=typeof o.compatRange==="string"?o.compatRange:null;i.capabilities=Array.isArray(o.capabilities)?o.capabilities.filter((c)=>typeof c==="string"):[];i.lastSeenAt=Date.now();i.banner?.dispose();i.banner=F(i.capabilities)?new ce((c)=>D(i,c),(c)=>a.warn(c)):null;i.banner?.start();if(F(i.capabilities))ge(i);a.info(`[daemon] Extension profile connected: ${i.contextId}`);E.traceExtConnect(i.contextId,i.extensionVersion);return}if(o.type==="log"){if(o.level==="error")a.error(`[ext] ${o.msg}`);else if(o.level==="warn")a.warn(`[ext] ${o.msg}`);else a.info(`[ext] ${o.msg}`);xe({level:o.level,msg:o.msg,ts:o.ts??Date.now()});E.traceExtLog(o.level,o.msg);return}if(o.type==="ping")return;if(o.type==="event"){for(const i of x.values())if(i.ws===n)i.banner?.onEvent(String(o.name),o.args);return}const e=u.get(o.id);if(e){E.traceResult(o.id,!!o.ok,Date.now()-e.dispatchTs,typeof o.errorCode==="string"?o.errorCode:void 0,typeof o.page==="string"?o.page:void 0);I(o.id,e,{data:o})}}catch(o){const e=r.toString().slice(0,200);a.warn(`[daemon] Ignoring malformed WS message from extension: ${o instanceof Error?o.message:String(o)} (first 200 chars: ${JSON.stringify(e)})`)}});n.on("close",()=>{a.info("[daemon] Extension disconnected");clearInterval(s);X(n)});n.on("error",()=>{clearInterval(s);X(n)})});T.listen(C,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${C}`)});T.on("error",(n)=>{if(n.code==="EADDRINUSE"){a.error(`[daemon] Port ${C} already in use — another daemon is likely running. Exiting.`);process.exit(P.SERVICE_UNAVAIL)}a.error(`[daemon] Server error: ${n.message}`);process.exit(P.GENERIC_ERROR)});O.listen(A,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${A} (PublishPort extension)`)});O.on("error",(n)=>{a.warn(`[daemon] PublishPort port ${A} unavailable (${n.message}) — extension will fall back to ${C}`)});function L(){for(const[n,t]of u){const s=t.dispatched?new S("Daemon shutting down before the command completed.","daemon_shutting_down","The daemon is being replaced; a journaling extension replays the command result on retry.",503):(()=>{const r=W(t.contextId);return new S(r.message,r.errorCode,r.errorHint,r.status)})();I(n,t,{error:s})}u.clear();for(const n of x.values()){n.banner?.shutdown();n.ws.close()}O.close();T.close(()=>process.exit(P.SUCCESS));setTimeout(()=>{T.closeIdleConnections?.();O.closeIdleConnections?.();setTimeout(()=>process.exit(P.SUCCESS),500).unref()},100).unref()}process.on("SIGTERM",L);process.on("SIGINT",L);
@@ -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.47",
3
+ "version": "1.0.49",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false