publishport-opencli 1.0.6 → 1.0.7

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
@@ -34440,7 +34440,8 @@
34440
34440
  "likes",
34441
34441
  "time",
34442
34442
  "is_reply",
34443
- "reply_to"
34443
+ "reply_to",
34444
+ "images"
34444
34445
  ],
34445
34446
  "type": "js",
34446
34447
  "modulePath": "rednote/comments.js",
@@ -40283,6 +40284,46 @@
40283
40284
  "siteSession": "persistent",
40284
40285
  "defaultWindowMode": "foreground"
40285
40286
  },
40287
+ {
40288
+ "site": "toutiao",
40289
+ "name": "recommend",
40290
+ "description": "今日头条频道推荐流(公开 API,无需登录)",
40291
+ "access": "read",
40292
+ "domain": "www.toutiao.com",
40293
+ "strategy": "public",
40294
+ "browser": false,
40295
+ "args": [
40296
+ {
40297
+ "name": "category",
40298
+ "type": "string",
40299
+ "default": "__all__",
40300
+ "required": false,
40301
+ "help": "频道 (__all__, news_tech, news_finance, news_world, news_sports, news_entertainment, news_military)"
40302
+ },
40303
+ {
40304
+ "name": "limit",
40305
+ "type": "int",
40306
+ "default": 20,
40307
+ "required": false,
40308
+ "help": "返回条数 (1-50)"
40309
+ }
40310
+ ],
40311
+ "columns": [
40312
+ "rank",
40313
+ "group_id",
40314
+ "title",
40315
+ "abstract",
40316
+ "source",
40317
+ "tag",
40318
+ "comments",
40319
+ "published_at",
40320
+ "url",
40321
+ "image_url"
40322
+ ],
40323
+ "type": "js",
40324
+ "modulePath": "toutiao/recommend.js",
40325
+ "sourceFile": "toutiao/recommend.js"
40326
+ },
40286
40327
  {
40287
40328
  "site": "toutiao",
40288
40329
  "name": "whoami",
@@ -47794,7 +47835,8 @@
47794
47835
  "likes",
47795
47836
  "time",
47796
47837
  "is_reply",
47797
- "reply_to"
47838
+ "reply_to",
47839
+ "images"
47798
47840
  ],
47799
47841
  "type": "js",
47800
47842
  "modulePath": "xiaohongshu/comments.js",
@@ -1,4 +1,4 @@
1
- import{AuthRequiredError as i,CommandExecutionError as s}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as r}from"../_shared/site-auth.js";async function a(e){return(await e.getCookies({url:"https://chatgpt.com"})).some((n)=>n.name==="__Secure-next-auth.session-token"&&n.value)}async function o(e){if(!await a(e))throw new i("chatgpt.com","ChatGPT __Secure-next-auth.session-token cookie missing");await e.goto("https://chatgpt.com/");await e.wait(2);const t=await e.evaluate(`(async () => {
1
+ import{AuthRequiredError as G,CommandExecutionError as D}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as K}from"../_shared/site-auth.js";async function H(B){return(await B.getCookies({url:"https://chatgpt.com"})).some((F)=>F.name.startsWith("__Secure-next-auth.session-token")&&F.value)}async function J(B){await B.goto("https://chatgpt.com/");await B.wait(2);const z=await B.evaluate(`(async () => {
2
2
  try {
3
3
  const res = await fetch('/api/auth/session', { credentials: 'include' });
4
4
  if (res.status === 401 || res.status === 403) {
@@ -14,4 +14,4 @@ import{AuthRequiredError as i,CommandExecutionError as s}from"@jackwener/opencli
14
14
  } catch (e) {
15
15
  return { kind: 'exception', detail: String(e && e.message || e) };
16
16
  }
17
- })()`);if(t?.kind==="auth")throw new i("chatgpt.com",t.detail);if(t?.kind==="http")throw new s(`HTTP ${t.httpStatus} from /api/auth/session`);if(t?.kind==="exception")throw new s(`ChatGPT whoami failed: ${t.detail}`);if(!t?.ok)throw new s(`Unexpected ChatGPT probe: ${JSON.stringify(t)}`);return{user_id:t.user_id,name:t.name}}r({site:"chatgpt",domain:"chatgpt.com",loginUrl:"https://auth.openai.com/log-in",columns:["user_id","name"],quickCheck:a,verify:o,poll:async(e)=>{if(!await a(e))throw new i("chatgpt.com","Waiting for ChatGPT session cookie");return o(e)}});
17
+ })()`);if(z?.kind==="auth")throw new G("chatgpt.com",z.detail);if(z?.kind==="http")throw new D(`HTTP ${z.httpStatus} from /api/auth/session`);if(z?.kind==="exception")throw new D(`ChatGPT whoami failed: ${z.detail}`);if(!z?.ok)throw new D(`Unexpected ChatGPT probe: ${JSON.stringify(z)}`);return{user_id:z.user_id,name:z.name}}K({site:"chatgpt",domain:"chatgpt.com",loginUrl:"https://auth.openai.com/log-in",columns:["user_id","name"],quickCheck:H,verify:J,poll:async(B)=>{if(!await H(B))throw new G("chatgpt.com","Waiting for ChatGPT session cookie");return J(B)}});
@@ -1 +1 @@
1
- import*as H from"node:os";import*as $ from"node:path";import*as I from"node:fs";import{cli as f,Strategy as E}from"@jackwener/opencli/registry";import{saveBase64ToFile as T}from"@jackwener/opencli/utils";import{ArgumentError as y,CommandExecutionError as _,EmptyResultError as v}from"@jackwener/opencli/errors";import{clearChatGPTDraft as C,getChatGPTVisibleImageUrls as x,navigateToProject as k,normalizeBooleanFlag as c,prepareChatGPTImagePaths as w,sendChatGPTMessage as P,unwrapEvaluateResult as h,waitForChatGPTImages as d,getChatGPTImageAssets as u,uploadChatGPTImages as n}from"./utils.js";const N="chatgpt.com";function m(q){if(q.includes("png"))return".png";if(q.includes("webp"))return".webp";if(q.includes("gif"))return".gif";return".jpg"}function l(q){const J=H.homedir();return q.startsWith(J)?`~${q.slice(J.length)}`:q}export function resolveOutputDir(q){const J=String(q||"").trim();if(!J)return $.join(H.homedir(),"Pictures","chatgpt");if(J==="~")return H.homedir();if(J.startsWith("~/"))return $.join(H.homedir(),J.slice(2));return $.resolve(J)}export function nextAvailablePath(q,J,z,W=I.existsSync){let Y=$.join(q,`${J}${z}`);for(let X=1;W(Y);X+=1)Y=$.join(q,`${J}_${X}${z}`);return Y}export function parseImagePaths(q){if(Array.isArray(q))return q.flatMap((J)=>parseImagePaths(J));return String(q??"").split(",").map((J)=>J.trim()).filter(Boolean)}function g(q,J){if(J>0)return`Edit the attached image${J===1?"":"s"}: ${q}`;return`Generate an image of: ${q}`}async function F(q){const J=h(await q.evaluate("window.location.href").catch(()=>""));return typeof J==="string"&&J?J:"https://chatgpt.com"}export const imageCommand=f({site:"chatgpt",name:"image",access:"write",description:"Generate images with ChatGPT web and save them locally",domain:N,strategy:E.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",positional:!0,required:!0,help:"Image prompt to send to ChatGPT"},{name:"image",help:"Local image path to attach before prompting; comma-separated paths are supported"},{name:"project",valueRequired:!0,help:"Start image generation inside a ChatGPT project ID or /g/g-p-<id> URL"},{name:"op",help:"Output directory (default: ~/Pictures/chatgpt)"},{name:"sd",type:"boolean",default:!1,help:"Skip download shorthand; only show ChatGPT link"},{name:"timeout",type:"int",required:!1,default:240,help:"Max seconds for the overall command (default: 240)"}],columns:["status","file","link"],func:async(q,J)=>{const z=J.prompt,W=parseImagePaths(J.image),Y=resolveOutputDir(J.op),X=J.sd,R=X===""||X===!0||c(X),O=J.timeout;if(!Number.isInteger(O)||O<1)throw new y("--timeout must be a positive integer (seconds)");const S=W.length?await w(W):{ok:!0,paths:[]};if(!S.ok)throw new y(S.reason);if(J.project)await k(q,J.project);else await q.goto(`https://${N}/new`,{settleMs:2000});await C(q);if(W.length){let K;try{K=await n(q,S.paths)}catch(Q){throw new _(`Failed to upload image to ChatGPT: ${Q instanceof Error?Q.message:String(Q)}`)}if(!K?.ok)throw new _(K?.reason||"Failed to upload image to ChatGPT")}const A=await x(q);if(!await P(q,g(z,W.length)))throw new _("Failed to send image prompt to ChatGPT",`Open ${await F(q)} and verify the composer is ready.`);let Z="";for(let K=0;K<10;K++){const Q=await F(q);if(Q.includes("/c/")){Z=Q;break}await q.wait(2)}if(!Z)Z=await F(q);const L=await d(q,A,O,Z),V=Z;if(!L.length)throw new v("chatgpt image",`No generated images were detected before timeout. Open ${V} and verify whether ChatGPT finished generating the image.`);if(R)return[{status:"\uD83C\uDFA8 generated",file:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${V}`}];const B=await u(q,L);if(!B.length)throw new _("Failed to export generated ChatGPT image assets",`Open ${V} and verify the generated images are visible, then retry.`);const U=Date.now(),M=[];for(let K=0;K<B.length;K+=1){const Q=B[K],D=Q.dataUrl.replace(/^data:[^;]+;base64,/,""),b=B.length>1?`_${K+1}`:"",G=m(Q.mimeType),j=nextAvailablePath(Y,`chatgpt_${U}${b}`,G);await T(D,j);M.push({status:"✅ saved",file:`\uD83D\uDCC1 ${l(j)}`,link:`\uD83D\uDD17 ${V}`})}return M}});
1
+ import*as H from"node:os";import*as $ from"node:path";import*as I from"node:fs";import{cli as f,Strategy as E}from"@jackwener/opencli/registry";import{saveBase64ToFile as T}from"@jackwener/opencli/utils";import{ArgumentError as y,CommandExecutionError as _,EmptyResultError as v}from"@jackwener/opencli/errors";import{clearChatGPTDraft as C,getChatGPTVisibleImageUrls as x,navigateToProject as k,normalizeBooleanFlag as c,prepareChatGPTImagePaths as w,sendChatGPTMessage as P,unwrapEvaluateResult as h,waitForChatGPTImages as d,getChatGPTImageAssets as u,uploadChatGPTImages as n}from"./utils.js";const N="chatgpt.com";function m(q){if(q.includes("png"))return".png";if(q.includes("webp"))return".webp";if(q.includes("gif"))return".gif";return".jpg"}function l(q){const J=H.homedir();return q.startsWith(J)?`~${q.slice(J.length)}`:q}export function resolveOutputDir(q){const J=String(q||"").trim();if(!J)return $.join(H.homedir(),"Pictures","chatgpt");if(J==="~")return H.homedir();if(J.startsWith("~/"))return $.join(H.homedir(),J.slice(2));return $.resolve(J)}export function nextAvailablePath(q,J,z,W=I.existsSync){let Y=$.join(q,`${J}${z}`);for(let X=1;W(Y);X+=1)Y=$.join(q,`${J}_${X}${z}`);return Y}export function parseImagePaths(q){if(Array.isArray(q))return q.flatMap((J)=>parseImagePaths(J));return String(q??"").split(",").map((J)=>J.trim()).filter(Boolean)}function g(q,J){if(J>0)return`Edit the attached image${J===1?"":"s"}: ${q}`;return`Generate an image of: ${q}`}async function F(q){const J=h(await q.evaluate("window.location.href").catch(()=>""));return typeof J==="string"&&J?J:"https://chatgpt.com"}export const imageCommand=f({site:"chatgpt",name:"image",access:"write",description:"Generate images with ChatGPT web and save them locally",domain:N,strategy:E.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",positional:!0,required:!0,help:"Image prompt to send to ChatGPT"},{name:"image",help:"Local image path to attach before prompting; comma-separated paths are supported"},{name:"project",valueRequired:!0,help:"Start image generation inside a ChatGPT project ID or /g/g-p-<id> URL"},{name:"op",help:"Output directory (default: ~/Pictures/chatgpt)"},{name:"sd",type:"boolean",default:!1,help:"Skip download shorthand; only show ChatGPT link"},{name:"timeout",type:"int",required:!1,default:240,help:"Max seconds for the overall command (default: 240)"}],columns:["status","file","link"],func:async(q,J)=>{const z=J.prompt,W=parseImagePaths(J.image),Y=resolveOutputDir(J.op),X=J.sd,R=X===""||X===!0||c(X),O=J.timeout;if(!Number.isInteger(O)||O<1)throw new y("--timeout must be a positive integer (seconds)");const S=W.length?await w(W):{ok:!0,paths:[]};if(!S.ok)throw new y(S.reason);if(J.project)await k(q,J.project);else await q.goto(`https://${N}/new`,{settleMs:2000});await C(q);if(W.length){let K;try{K=await n(q,S.paths)}catch(Q){throw new _(`Failed to upload image to ChatGPT: ${Q instanceof Error?Q.message:String(Q)}`)}if(!K?.ok)throw new _(K?.reason||"Failed to upload image to ChatGPT")}const A=await x(q);if(!await P(q,g(z,W.length)))throw new _("Failed to send image prompt to ChatGPT",`Open ${await F(q)} and verify the composer is ready.`);let Z="";for(let K=0;K<10;K++){const Q=await F(q);if(Q.includes("/c/")){Z=Q;break}await q.sleep(2)}if(!Z)Z=await F(q);const L=await d(q,A,O,Z),V=Z;if(!L.length)throw new v("chatgpt image",`No generated images were detected before timeout. Open ${V} and verify whether ChatGPT finished generating the image.`);if(R)return[{status:"\uD83C\uDFA8 generated",file:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${V}`}];const B=await u(q,L);if(!B.length)throw new _("Failed to export generated ChatGPT image assets",`Open ${V} and verify the generated images are visible, then retry.`);const U=Date.now(),M=[];for(let K=0;K<B.length;K+=1){const Q=B[K],D=Q.dataUrl.replace(/^data:[^;]+;base64,/,""),b=B.length>1?`_${K+1}`:"",G=m(Q.mimeType),j=nextAvailablePath(Y,`chatgpt_${U}${b}`,G);await T(D,j);M.push({status:"✅ saved",file:`\uD83D\uDCC1 ${l(j)}`,link:`\uD83D\uDD17 ${V}`})}return M}});
@@ -701,7 +701,7 @@ import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError a
701
701
  }
702
702
  return urls;
703
703
  })()
704
- `)),"chatgpt visible image url extraction")}export async function waitForChatGPTImages(Q,X,Y,Z){const $=new Set(X),W=3,V=Math.max(1,Math.ceil(Y/W));let z=[],q=0;for(let j=0;j<V;j++){await Q.wait(j===0?3:W);let J="";if(Z&&Z.includes("/c/")){J=unwrapEvaluateResult(await Q.evaluate("window.location.href").catch(()=>""));if(J&&!N(J,Z)){await Q.goto(Z);await Q.wait(3)}}if(await isGenerating(Q))continue;if(Z&&Z.includes("/c/")&&j>0&&j%5===0){if(!J||N(J,Z)){await Q.goto(Z);await Q.wait(3)}}const B=(await getChatGPTVisibleImageUrls(Q)).filter((b)=>!$.has(b));if(B.length===0)continue;const H=B.join(`
704
+ `)),"chatgpt visible image url extraction")}export async function waitForChatGPTImages(Q,X,Y,Z){const $=new Set(X),W=3,V=Math.max(1,Math.ceil(Y/W));let z=[],q=0;for(let j=0;j<V;j++){await Q.wait(j===0?3:W);let J="";if(Z&&Z.includes("/c/")){J=unwrapEvaluateResult(await Q.evaluate("window.location.href").catch(()=>""));if(J&&!N(J,Z)){await Q.goto(Z);await Q.sleep(3)}}if(await isGenerating(Q))continue;if(Z&&Z.includes("/c/")&&j>0&&j%5===0){if(!J||N(J,Z)){await Q.goto(Z);await Q.sleep(3)}}const B=(await getChatGPTVisibleImageUrls(Q)).filter((b)=>!$.has(b));if(B.length===0)continue;const H=B.join(`
705
705
  `),D=z.join(`
706
706
  `);if(H===D)q+=1;else{z=B;q=1}if(q>=2||j===V-1)return z}return z}export async function getProjectList(Q){await ensureOnChatGPT(Q);if(requireBooleanEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`(() => {
707
707
  const button = Array.from(document.querySelectorAll('button'))
@@ -9,19 +9,44 @@ import{cli as e}from"@jackwener/opencli/registry";e({site:"instagram",name:"expl
9
9
  );
10
10
  if (!res.ok) throw new Error('HTTP ' + res.status + ' - make sure you are logged in to Instagram');
11
11
  const data = await res.json();
12
- const posts = [];
13
- for (const sec of (data?.sectional_items || [])) {
14
- for (const m of (sec?.layout_content?.medias || [])) {
15
- const media = m?.media;
16
- if (media) posts.push({
17
- user: media.user?.username || '',
18
- caption: (media.caption?.text || '').replace(/\\n/g, ' ').substring(0, 100),
19
- likes: media.like_count ?? 0,
20
- comments: media.comment_count ?? 0,
21
- type: media.media_type === 1 ? 'photo' : media.media_type === 2 ? 'video' : 'carousel',
22
- });
12
+
13
+ // Instagram no longer populates the flat layout_content.medias[] path. Media
14
+ // objects are now nested across mixed layout shapes (one_by_two_item.clips.
15
+ // items[].media, fill_items[].media, etc.), so recursively walk each sectional
16
+ // item collecting every distinct node.media and dedupe by pk/id/code. See #2091.
17
+ const seen = new Set();
18
+ const medias = [];
19
+ const collect = (node, depth) => {
20
+ if (!node || typeof node !== 'object' || depth > 8) return;
21
+ if (Array.isArray(node)) {
22
+ for (const item of node) collect(item, depth + 1);
23
+ return;
23
24
  }
24
- }
25
+ const media = node.media;
26
+ if (media && typeof media === 'object' && !Array.isArray(media)) {
27
+ const key = media.pk ?? media.id ?? media.code;
28
+ if (key != null && !seen.has(key)) {
29
+ seen.add(key);
30
+ medias.push(media);
31
+ }
32
+ }
33
+ for (const [k, value] of Object.entries(node)) {
34
+ // Don't descend into a collected media object — a carousel's child items
35
+ // carry their own .media and would otherwise be counted as separate posts.
36
+ if (k === 'media') continue;
37
+ if (value && typeof value === 'object') collect(value, depth + 1);
38
+ }
39
+ };
40
+ for (const sec of (data?.sectional_items || [])) collect(sec, 0);
41
+
42
+ const posts = medias.map((media) => ({
43
+ user: media.user?.username || '',
44
+ caption: (media.caption?.text || '').replace(/\\n/g, ' ').substring(0, 100),
45
+ // Clips/reels report engagement via play_count rather than like_count.
46
+ likes: media.like_count ?? media.play_count ?? 0,
47
+ comments: media.comment_count ?? 0,
48
+ type: media.media_type === 1 ? 'photo' : media.media_type === 2 ? 'video' : 'carousel',
49
+ }));
25
50
  return posts.slice(0, limit).map((p, i) => ({ rank: i + 1, ...p }));
26
51
  })()
27
52
  `}]});
@@ -1 +1 @@
1
- import{cli as p,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as u,AuthRequiredError as h,CommandExecutionError as w,EmptyResultError as y}from"@jackwener/opencli/errors";import{buildCommentsExtractJs as b}from"../xiaohongshu/comments.js";import{buildNoteUrl as g,parseNoteId as x}from"../xiaohongshu/note-helpers.js";const R="Pass a full rednote.com note URL with xsec_token from search results or user/profile context.";function _(o){const e=Number(o??20);if(!Number.isFinite(e)||!Number.isInteger(e))throw new u(`--limit must be an integer between 1 and 50, got ${JSON.stringify(o)}`);if(e<1||e>50)throw new u(`--limit must be between 1 and 50, got ${e}`);return e}p({site:"rednote",name:"comments",access:"read",description:"Read comments from a rednote note (supports nested replies)",domain:"www.rednote.com",strategy:f.COOKIE,navigateBefore:!1,args:[{name:"note-id",required:!0,positional:!0,help:"Full rednote note URL with xsec_token"},{name:"limit",type:"int",default:20,help:"Number of top-level comments (max 50)"},{name:"with-replies",type:"boolean",default:!1,help:"Include nested replies (楼中楼)"}],columns:["rank","author","text","likes","time","is_reply","reply_to"],func:async(o,e)=>{const s=_(e.limit),a=Boolean(e["with-replies"]),i=String(e["note-id"]),d=x(i);await o.goto(g(i,{commandName:"rednote comments",cookieRoot:"rednote.com",signedUrlHint:R}));await o.wait({time:2+Math.random()*3});const r=await o.evaluate(b(a));if(!r||typeof r!=="object")throw new y("rednote/comments","Unexpected evaluate response");if(r.securityBlock)throw new w("Rednote security block: the note detail page was blocked by risk control.",/^https?:\/\//.test(i)?"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(r.loginWall)throw new h("www.rednote.com","Note comments require login");const m=r.results??[],l=(t,n)=>({rank:n+1,author:t.author,text:t.text,likes:t.likes,time:t.time,is_reply:t.is_reply,reply_to:t.reply_to});if(a){const t=[];let n=0;for(const c of m){if(!c.is_reply)n++;if(n>s)break;t.push(c)}return t.map(l)}return m.slice(0,s).map(l)}});
1
+ import{cli as p,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as u,AuthRequiredError as h,CommandExecutionError as w,EmptyResultError as y}from"@jackwener/opencli/errors";import{buildCommentsExtractJs as g,normalizeCommentRows as b}from"../xiaohongshu/comments.js";import{buildNoteUrl as R,parseNoteId as x}from"../xiaohongshu/note-helpers.js";const _="Pass a full rednote.com note URL with xsec_token from search results or user/profile context.";function k(o){const e=Number(o??20);if(!Number.isFinite(e)||!Number.isInteger(e))throw new u(`--limit must be an integer between 1 and 50, got ${JSON.stringify(o)}`);if(e<1||e>50)throw new u(`--limit must be between 1 and 50, got ${e}`);return e}p({site:"rednote",name:"comments",access:"read",description:"Read comments from a rednote note (supports nested replies)",domain:"www.rednote.com",strategy:f.COOKIE,navigateBefore:!1,args:[{name:"note-id",required:!0,positional:!0,help:"Full rednote note URL with xsec_token"},{name:"limit",type:"int",default:20,help:"Number of top-level comments (max 50)"},{name:"with-replies",type:"boolean",default:!1,help:"Include nested replies (楼中楼)"}],columns:["rank","author","text","likes","time","is_reply","reply_to","images"],func:async(o,e)=>{const i=k(e.limit),m=Boolean(e["with-replies"]),s=String(e["note-id"]),d=x(s);await o.goto(R(s,{commandName:"rednote comments",cookieRoot:"rednote.com",signedUrlHint:_}));await o.wait({time:2+Math.random()*3});const r=await o.evaluate(g(m,i));if(!r||typeof r!=="object")throw new y("rednote/comments","Unexpected evaluate response");if(r.securityBlock)throw new w("Rednote security block: the note detail page was blocked by risk control.",/^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(r.loginWall)throw new h("www.rednote.com","Note comments require login");const a=b(r.results,"rednote/comments"),l=(t,n)=>({rank:n+1,author:t.author,text:t.text,likes:t.likes,time:t.time,is_reply:t.is_reply,reply_to:t.reply_to,images:t.images??[]});if(m){const t=[];let n=0;for(const c of a){if(!c.is_reply)n++;if(n>i)break;t.push(c)}return t.map(l)}return a.slice(0,i).map(l)}});
@@ -1 +1 @@
1
- import{cli as M,Strategy as P}from"@jackwener/opencli/registry";import{CommandExecutionError as z,EmptyResultError as Q}from"@jackwener/opencli/errors";import{HOT_BOARD_URL as V,mapHotRow as W,parseHotLimit as X}from"./utils.js";M({site:"toutiao",name:"hot",access:"read",description:"今日头条首页热榜(公开 API,无需登录)",domain:"www.toutiao.com",strategy:P.PUBLIC,browser:!1,args:[{name:"limit",type:"int",default:30,help:"返回条数 (1-50)"}],columns:["rank","group_id","title","query","hot_value","label","url","image_url"],func:async(Y,I)=>{const J=X(I?.limit,30);let F;try{F=await fetch(V,{headers:{"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",Accept:"application/json",Referer:"https://www.toutiao.com/"}})}catch(q){throw new z(`toutiao hot-board request failed: ${q?.message||q}`)}if(!F.ok)throw new z(`toutiao hot-board failed: HTTP ${F.status}`);let b;try{b=await F.json()}catch(q){throw new z(`toutiao hot-board returned malformed JSON: ${q?.message||q}`)}if(b?.status&&b.status!=="success")throw new z(`toutiao hot-board returned status=${b.status}`);if(b?.error||b?.message)throw new z(`toutiao hot-board returned error: ${b.error||b.message}`);const G=(Array.isArray(b?.data)?b.data:[]).map(W).filter(Boolean).slice(0,J);if(G.length===0)throw new Q("toutiao hot","上游 hot-board 返回空列表。");return G.map((q,K)=>({...q,rank:K+1}))}});
1
+ import{cli as M,Strategy as P}from"@jackwener/opencli/registry";import{CommandExecutionError as z,EmptyResultError as Q}from"@jackwener/opencli/errors";import{HOT_BOARD_URL as V,mapHotRow as W,parseHotLimit as X}from"./utils.js";M({site:"toutiao",name:"hot",access:"read",description:"今日头条首页热榜(公开 API,无需登录)",domain:"www.toutiao.com",strategy:P.PUBLIC,browser:!1,args:[{name:"limit",type:"int",default:30,help:"返回条数 (1-50)"}],columns:["rank","group_id","title","query","hot_value","label","url","image_url"],func:async(I)=>{const J=X(I?.limit,30);let F;try{F=await fetch(V,{headers:{"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",Accept:"application/json",Referer:"https://www.toutiao.com/"}})}catch(q){throw new z(`toutiao hot-board request failed: ${q?.message||q}`)}if(!F.ok)throw new z(`toutiao hot-board failed: HTTP ${F.status}`);let b;try{b=await F.json()}catch(q){throw new z(`toutiao hot-board returned malformed JSON: ${q?.message||q}`)}if(b?.status&&b.status!=="success")throw new z(`toutiao hot-board returned status=${b.status}`);if(b?.error||b?.message)throw new z(`toutiao hot-board returned error: ${b.error||b.message}`);const G=(Array.isArray(b?.data)?b.data:[]).map(W).filter(Boolean).slice(0,J);if(G.length===0)throw new Q("toutiao hot","上游 hot-board 返回空列表。");return G.map((q,K)=>({...q,rank:K+1}))}});
@@ -0,0 +1 @@
1
+ import{cli as V,Strategy as W}from"@jackwener/opencli/registry";import{CommandExecutionError as z,EmptyResultError as X}from"@jackwener/opencli/errors";import{RECOMMEND_CATEGORIES as Y,RECOMMEND_URL as Z,mapRecommendRow as $,parseRecommendCategory as h,parseRecommendLimit as v}from"./utils.js";V({site:"toutiao",name:"recommend",access:"read",description:"今日头条频道推荐流(公开 API,无需登录)",domain:"www.toutiao.com",strategy:W.PUBLIC,browser:!1,args:[{name:"category",type:"string",default:"__all__",help:`频道 (${Y.join(", ")})`},{name:"limit",type:"int",default:20,help:"返回条数 (1-50)"}],columns:["rank","group_id","title","abstract","source","tag","comments","published_at","url","image_url"],func:async(F)=>{const H=h(F?.category,"__all__"),K=v(F?.limit,20),P=`${Z}?category=${encodeURIComponent(H)}`;let B;try{B=await fetch(P,{headers:{"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",Accept:"application/json",Referer:"https://www.toutiao.com/"}})}catch(b){throw new z(`toutiao recommend request failed: ${b?.message||b}`)}if(!B.ok)throw new z(`toutiao recommend failed: HTTP ${B.status}`);let q;try{q=await B.json()}catch(b){throw new z(`toutiao recommend returned malformed JSON: ${b?.message||b}`)}if(q?.message&&q.message!=="success")throw new z(`toutiao recommend returned message=${q.message}`);if(!Array.isArray(q?.data))throw new z("toutiao recommend returned a non-array data field");const J=q.data.map($).filter(Boolean).slice(0,K);if(J.length===0)throw new X("toutiao recommend",`频道 ${H} 返回空列表。`);return J.map((b,Q)=>({...b,rank:Q+1}))}});
@@ -1,2 +1,2 @@
1
- import{ArgumentError as V}from"@jackwener/opencli/errors";const W=1,Y=4,Z=1,$=50;export function parseArticlesPage(q,z=1){if(q===void 0||q===null||q==="")return z;const B=Number(q);if(!Number.isFinite(B)||!Number.isInteger(B))throw new V(`--page must be an integer between ${W} and ${Y}, got ${JSON.stringify(q)}`);if(B<W||B>Y)throw new V(`--page must be between ${W} and ${Y}, got ${B}`);return B}export function parseHotLimit(q,z=30){if(q===void 0||q===null||q==="")return z;const B=Number(q);if(!Number.isFinite(B)||!Number.isInteger(B))throw new V(`--limit must be an integer between ${Z} and ${$}, got ${JSON.stringify(q)}`);if(B<Z||B>$)throw new V(`--limit must be between ${Z} and ${$}, got ${B}`);return B}const x=new Set(["展现","阅读","点赞","评论","查看数据","查看评论","修改","更多","首发","已发布","定时发布","定时发布中","由文章生成","审核中"]),C=/展现\s*([\d,]+)\s*阅读\s*([\d,]+)\s*点赞\s*([\d,]+)\s*评论\s*([\d,]*)/;export function parseToutiaoArticlesText(q){const z=String(q||"").split(`
2
- `).map((D)=>D.trim()).filter(Boolean),B=[];for(let D=0;D<z.length;D++){const y=z[D];if(!/^\d{2}-\d{2}\s+\d{2}:\d{2}$/.test(y))continue;const h=y;let U=null,j=null,g=null;for(let J=3;J>=1;J--){const F=z[D-J]||"";if(!F||F.length>=100||/^\d+$/.test(F)||x.has(F))continue;U=F;break}for(let J=1;J<8;J++){const F=z[D+J]||"";if(F==="已发布"||F==="定时发布中"||F==="审核中"||F==="由文章生成")j=F;if(F.includes("展现")&&F.includes("阅读")){const K=F.match(C);if(K)g={"展现":K[1],"阅读":K[2],"点赞":K[3],"评论":K[4]||"0"}}}if(!U)continue;if(g)B.push({title:U,date:h,status:j,...g});else B.push({title:U,date:h,status:j,"展现":null,"阅读":null,"点赞":null,"评论":null})}return B}function Q(q){const z=String(q??"").trim();return z?z:null}function G(q){const z=q?.Image?.url;if(typeof z==="string"&&z)return z;return(Array.isArray(q?.Image?.url_list)?q.Image.url_list.map((D)=>typeof D==="string"?D:D?.url).find((D)=>typeof D==="string"&&D):null)||null}function X(q){const z=Number(q);return Number.isFinite(z)&&z>=0?z:null}export function mapHotRow(q,z){if(!q||typeof q!=="object")return null;const B=Q(q.ClusterIdStr||(q.ClusterId!=null?String(q.ClusterId):null)),D=Q(q.Title);if(!D)return null;return{rank:z+1,group_id:B,title:D,query:Q(q.QueryWord)||D,hot_value:X(q.HotValue),label:Q(q.Label),url:Q(q.Url),image_url:G(q)}}export const HOT_BOARD_URL="https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc";export function looksToutiaoAuthWallText(q){const z=String(q||"").replace(/\s+/g," ").trim().toLowerCase();if(!z)return!1;return/登录|请登录|账号登录|扫码登录|安全验证|验证码|captcha/.test(z)||/\b(login|sign in|captcha|verification required)\b/.test(z)||/mp\.toutiao\.com\/profile_v4\/login/.test(z)}export const __test__={ARTICLES_MIN_PAGE:W,ARTICLES_MAX_PAGE:Y,HOT_MIN_LIMIT:Z,HOT_MAX_LIMIT:$};
1
+ import{ArgumentError as Z,CommandExecutionError as B}from"@jackwener/opencli/errors";const U=1,D=4,H=1,y=50,P=1,X=50;export function parseArticlesPage(q,z=1){if(q===void 0||q===null||q==="")return z;const J=Number(q);if(!Number.isFinite(J)||!Number.isInteger(J))throw new Z(`--page must be an integer between ${U} and ${D}, got ${JSON.stringify(q)}`);if(J<U||J>D)throw new Z(`--page must be between ${U} and ${D}, got ${J}`);return J}export function parseRecommendLimit(q,z=20){if(q===void 0||q===null||q==="")return z;const J=Number(q);if(!Number.isFinite(J)||!Number.isInteger(J))throw new Z(`--limit must be an integer between ${P} and ${X}, got ${JSON.stringify(q)}`);if(J<P||J>X)throw new Z(`--limit must be between ${P} and ${X}, got ${J}`);return J}export function parseRecommendCategory(q,z="__all__"){if(q===void 0||q===null||q==="")return z;const J=String(q).trim();if(!RECOMMEND_CATEGORIES.includes(J))throw new Z(`--category must be one of ${RECOMMEND_CATEGORIES.join(", ")}, got ${JSON.stringify(q)}`);return J}export function parseHotLimit(q,z=30){if(q===void 0||q===null||q==="")return z;const J=Number(q);if(!Number.isFinite(J)||!Number.isInteger(J))throw new Z(`--limit must be an integer between ${H} and ${y}, got ${JSON.stringify(q)}`);if(J<H||J>y)throw new Z(`--limit must be between ${H} and ${y}, got ${J}`);return J}const b=new Set(["展现","阅读","点赞","评论","查看数据","查看评论","修改","更多","首发","已发布","定时发布","定时发布中","由文章生成","审核中"]),g=/展现\s*([\d,]+)\s*阅读\s*([\d,]+)\s*点赞\s*([\d,]+)\s*评论\s*([\d,]*)/;export function parseToutiaoArticlesText(q){const z=String(q||"").split(`
2
+ `).map((K)=>K.trim()).filter(Boolean),J=[];for(let K=0;K<z.length;K++){const $=z[K];if(!/^\d{2}-\d{2}\s+\d{2}:\d{2}$/.test($))continue;const W=$;let F=null,h=null,k=null;for(let Y=3;Y>=1;Y--){const Q=z[K-Y]||"";if(!Q||Q.length>=100||/^\d+$/.test(Q)||b.has(Q))continue;F=Q;break}for(let Y=1;Y<8;Y++){const Q=z[K+Y]||"";if(Q==="已发布"||Q==="定时发布中"||Q==="审核中"||Q==="由文章生成")h=Q;if(Q.includes("展现")&&Q.includes("阅读")){const j=Q.match(g);if(j)k={"展现":j[1],"阅读":j[2],"点赞":j[3],"评论":j[4]||"0"}}}if(!F)continue;if(k)J.push({title:F,date:W,status:h,...k});else J.push({title:F,date:W,status:h,"展现":null,"阅读":null,"点赞":null,"评论":null})}return J}function V(q){const z=String(q??"").trim();return z?z:null}function v(q){const z=q?.Image?.url;if(typeof z==="string"&&z)return z;return(Array.isArray(q?.Image?.url_list)?q.Image.url_list.map((K)=>typeof K==="string"?K:K?.url).find((K)=>typeof K==="string"&&K):null)||null}function S(q){const z=Number(q);return Number.isFinite(z)&&z>=0?z:null}export function mapHotRow(q,z){if(!q||typeof q!=="object")return null;const J=V(q.ClusterIdStr||(q.ClusterId!=null?String(q.ClusterId):null)),K=V(q.Title);if(!K)return null;return{rank:z+1,group_id:J,title:K,query:V(q.QueryWord)||K,hot_value:S(q.HotValue),label:V(q.Label),url:V(q.Url),image_url:v(q)}}export const HOT_BOARD_URL="https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc",RECOMMEND_URL="https://www.toutiao.com/api/pc/feed/",RECOMMEND_CATEGORIES=["__all__","news_tech","news_finance","news_world","news_sports","news_entertainment","news_military"];function G(q){const z=V(q?.image_url)||V(q?.middle_image?.url);if(!z)return null;return z.startsWith("//")?`https:${z}`:z}function x(q){const z=V(q);if(!z)return null;try{const J=z.startsWith("/")?new URL(z,"https://www.toutiao.com"):new URL(z);if(!/^https?:$/i.test(J.protocol)||!/(^|\.)toutiao\.com$/i.test(J.hostname))throw new B(`Malformed toutiao recommend row: off-domain source_url ${JSON.stringify(z)}.`);return J}catch(J){if(J instanceof B)throw J;throw new B(`Malformed toutiao recommend row: invalid source_url ${JSON.stringify(z)}.`)}}function R(q){return String(q||"").match(/^\/(?:group|article)\/([A-Za-z0-9_-]+)\/?$/)?.[1]||""}function f(q,z){const J=x(q),K=J?R(J.pathname):"",$=V(z),W=$||V(K);if(!W)throw new B("Malformed toutiao recommend row: missing group_id and article source_url.");if(!/^[A-Za-z0-9_-]+$/.test(W))throw new B(`Malformed toutiao recommend row: invalid group_id ${JSON.stringify(z)}.`);if(K&&$&&$!==K)throw new B(`Malformed toutiao recommend row: group_id/source_url mismatch for ${JSON.stringify(z)}.`);return{groupId:W,url:`https://www.toutiao.com/group/${W}/`}}function C(q){const z=Number(q);if(!Number.isFinite(z)||z<=0)return null;return`${new Date(z*1000).toISOString().slice(0,19)}Z`}export function mapRecommendRow(q,z){if(!q||typeof q!=="object")return null;if(q.is_feed_ad)return null;const J=V(q.title);if(!J)return null;const K=f(q.source_url,q.group_id!=null?String(q.group_id):null);return{rank:z+1,group_id:K.groupId,title:J,abstract:V(q.abstract),source:V(q.source),tag:V(q.chinese_tag),comments:S(q.comments_count),published_at:C(q.behot_time),url:K.url,image_url:G(q)}}export function looksToutiaoAuthWallText(q){const z=String(q||"").replace(/\s+/g," ").trim().toLowerCase();if(!z)return!1;return/登录|请登录|账号登录|扫码登录|安全验证|验证码|captcha/.test(z)||/\b(login|sign in|captcha|verification required)\b/.test(z)||/mp\.toutiao\.com\/profile_v4\/login/.test(z)}export const __test__={ARTICLES_MIN_PAGE:U,ARTICLES_MAX_PAGE:D,HOT_MIN_LIMIT:H,HOT_MAX_LIMIT:y,RECOMMEND_MIN_LIMIT:P,RECOMMEND_MAX_LIMIT:X};
@@ -1,4 +1,4 @@
1
- import{AuthRequiredError as Z,CommandExecutionError as G}from"@jackwener/opencli/errors";import{cli as $,Strategy as b}from"@jackwener/opencli/registry";import{resolveTwitterQueryId as K,describeTwitterApiError as L}from"./shared.js";import{TWITTER_BEARER_TOKEN as N}from"./utils.js";const O="7xflPyRiUxGVbJd4uWmbfg";$({site:"twitter",name:"article",access:"read",description:"Fetch a Twitter Article (long-form content) and export as Markdown",domain:"x.com",strategy:b.COOKIE,browser:!0,args:[{name:"tweet-id",type:"string",positional:!0,required:!0,help:"Tweet ID or URL containing the article"}],columns:["title","author","content","url"],func:async(f,P)=>{let j=P["tweet-id"];const V=/\/article\/\d+/.test(j),H=j.match(/\/(?:status|article)\/(\d+)/);if(H)j=H[1];if(V){await f.goto(`https://x.com/i/article/${j}`);await f.wait(3);const F=await f.evaluate(`
1
+ import{AuthRequiredError as Z,CommandExecutionError as H}from"@jackwener/opencli/errors";import{cli as N,Strategy as P}from"@jackwener/opencli/registry";import{resolveTwitterQueryId as b,describeTwitterApiError as k,unwrapBrowserResult as $}from"./shared.js";import{TWITTER_BEARER_TOKEN as B}from"./utils.js";const D="7xflPyRiUxGVbJd4uWmbfg";function M(f){return Boolean(f&&typeof f==="object"&&!Array.isArray(f))}N({site:"twitter",name:"article",access:"read",description:"Fetch a Twitter Article (long-form content) and export as Markdown",domain:"x.com",strategy:P.COOKIE,browser:!0,args:[{name:"tweet-id",type:"string",positional:!0,required:!0,help:"Tweet ID or URL containing the article"}],columns:["title","author","content","url"],func:async(f,j)=>{let F=j["tweet-id"];const K=/\/article\/\d+/.test(F),V=F.match(/\/(?:status|article)\/(\d+)/);if(V)F=V[1];if(K){await f.goto(`https://x.com/i/article/${F}`);await f.wait(3);const G=await f.evaluate(`
2
2
  (function() {
3
3
  var links = document.querySelectorAll('a[href*="/status/"]');
4
4
  for (var i = 0; i < links.length; i++) {
@@ -12,12 +12,12 @@ import{AuthRequiredError as Z,CommandExecutionError as G}from"@jackwener/opencli
12
12
  }
13
13
  return null;
14
14
  })()
15
- `);if(!F||typeof F!=="string")throw new G(`Could not resolve article ${j} to a tweet ID. The article page may not contain a linked tweet.`);j=F}await f.goto(`https://x.com/i/status/${j}`);await f.wait(3);const J=(await f.getCookies({url:"https://x.com"})).find((F)=>F.name==="ct0")?.value||null;if(!J)throw new Z("x.com","Not logged into x.com (no ct0 cookie)");const X=await K(f,"TweetResultByRestId",O),z=await f.evaluate(`
15
+ `),J=$(G);if(!J||typeof J!=="string")throw new H(`Could not resolve article ${F} to a tweet ID. The article page may not contain a linked tweet.`);F=J}await f.goto(`https://x.com/i/status/${F}`);await f.wait(3);const X=(await f.getCookies({url:"https://x.com"})).find((G)=>G.name==="ct0")?.value||null;if(!X)throw new Z("x.com","Not logged into x.com (no ct0 cookie)");const L=await b(f,"TweetResultByRestId",D),z=$(await f.evaluate(`
16
16
  async () => {
17
- const tweetId = "${j}";
18
- const ct0 = ${JSON.stringify(J)};
17
+ const tweetId = ${JSON.stringify(F)};
18
+ const ct0 = ${JSON.stringify(X)};
19
19
 
20
- const bearer = ${JSON.stringify(N)};
20
+ const bearer = ${JSON.stringify(B)};
21
21
  const headers = {
22
22
  'Authorization': 'Bearer ' + decodeURIComponent(bearer),
23
23
  'X-Csrf-Token': ct0,
@@ -45,23 +45,54 @@ import{AuthRequiredError as Z,CommandExecutionError as G}from"@jackwener/opencli
45
45
  withArticlePlainText: true,
46
46
  });
47
47
 
48
- const url = '/i/api/graphql/' + ${JSON.stringify(X)} + '/TweetResultByRestId?variables='
48
+ const url = '/i/api/graphql/' + ${JSON.stringify(L)} + '/TweetResultByRestId?variables='
49
49
  + encodeURIComponent(variables)
50
50
  + '&features=' + encodeURIComponent(features)
51
51
  + '&fieldToggles=' + encodeURIComponent(fieldToggles);
52
52
 
53
- const resp = await fetch(url, {headers, credentials: 'include'});
53
+ let resp;
54
+ try {
55
+ resp = await fetch(url, {headers, credentials: 'include'});
56
+ } catch (error) {
57
+ return {error: 'Twitter article request failed: ' + String(error && error.message || error)};
58
+ }
54
59
  if (!resp.ok) return {httpStatus: resp.status};
55
- const d = await resp.json();
60
+ let d;
61
+ try {
62
+ d = await resp.json();
63
+ } catch {
64
+ return {error: 'Twitter API response was not valid JSON', hint: 'You may be logged out or the request was blocked'};
65
+ }
66
+ if (!d || typeof d !== 'object' || Array.isArray(d)) {
67
+ return {error: 'Twitter API response payload was malformed'};
68
+ }
56
69
 
57
- const result = d.data?.tweetResult?.result;
58
- if (!result) return {error: 'Article not found'};
70
+ const result = d?.data?.tweetResult?.result;
71
+ if (!result) {
72
+ if (Array.isArray(d.errors) && d.errors.length > 0) {
73
+ return {error: 'Twitter TweetResultByRestId returned GraphQL errors: ' + JSON.stringify(d.errors).slice(0, 200)};
74
+ }
75
+ return {error: 'Article not found'};
76
+ }
59
77
 
60
78
  // Unwrap TweetWithVisibilityResults
79
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
80
+ return {error: 'Twitter API response tweet result was malformed'};
81
+ }
61
82
  const tw = result.tweet || result;
83
+ if (!tw || typeof tw !== 'object' || Array.isArray(tw)) {
84
+ return {error: 'Twitter API response tweet result was malformed'};
85
+ }
62
86
  const legacy = tw.legacy || {};
63
87
  const user = tw.core?.user_results?.result;
64
- const screenName = user?.legacy?.screen_name || user?.core?.screen_name || 'unknown';
88
+ const returnedTweetId = tw.rest_id || legacy.id_str;
89
+ if (typeof returnedTweetId !== 'string' || returnedTweetId !== tweetId) {
90
+ return {error: 'Twitter API response did not match requested tweet ' + tweetId};
91
+ }
92
+ const screenName = user?.legacy?.screen_name || user?.core?.screen_name || '';
93
+ if (typeof screenName !== 'string' || !/^[A-Za-z0-9_]{1,15}$/.test(screenName)) {
94
+ return {error: 'Twitter API response did not include a valid author screen name for tweet ' + tweetId};
95
+ }
65
96
 
66
97
  // Extract article content
67
98
  const articleResults = tw.article?.article_results?.result;
@@ -78,15 +109,25 @@ import{AuthRequiredError as Z,CommandExecutionError as G}from"@jackwener/opencli
78
109
  }
79
110
  return {error: 'Tweet ' + tweetId + ' has no article content'};
80
111
  }
112
+ if (!articleResults || typeof articleResults !== 'object' || Array.isArray(articleResults)) {
113
+ return {error: 'Twitter API response article result was malformed'};
114
+ }
81
115
 
82
116
  const title = articleResults.title || '(Untitled)';
83
117
  const contentState = articleResults.content_state || {};
118
+ if (!contentState || typeof contentState !== 'object' || Array.isArray(contentState)) {
119
+ return {error: 'Twitter API response article content was malformed'};
120
+ }
84
121
  const blocks = contentState.blocks || [];
122
+ if (!Array.isArray(blocks)) {
123
+ return {error: 'Twitter API response article blocks were malformed'};
124
+ }
85
125
 
86
126
  // Convert draft.js blocks to Markdown
87
127
  const parts = [];
88
128
  let orderedCounter = 0;
89
129
  for (const block of blocks) {
130
+ if (!block || typeof block !== 'object' || Array.isArray(block)) continue;
90
131
  const blockType = block.type || 'unstyled';
91
132
  if (blockType === 'atomic') continue;
92
133
  const text = block.text || '';
@@ -113,4 +154,4 @@ import{AuthRequiredError as Z,CommandExecutionError as G}from"@jackwener/opencli
113
154
  url: 'https://x.com/' + screenName + '/status/' + tweetId,
114
155
  }];
115
156
  }
116
- `);if(z?.httpStatus)throw new G(L("TweetResultByRestId",z.httpStatus));if(z?.error)throw new G(z.error+(z.hint?` (${z.hint})`:""));return z||[]}});
157
+ `));if(!Array.isArray(z)&&!M(z))throw new H("Twitter article response payload is malformed");if(z?.httpStatus){const G=k("TweetResultByRestId",z.httpStatus);if(z.httpStatus===401||z.httpStatus===403)throw new Z("x.com",G);throw new H(G)}if(z?.error)throw new H(z.error+(z.hint?` (${z.hint})`:""));if(!Array.isArray(z))throw new H("Twitter article response payload is malformed");return z}});
@@ -3,7 +3,7 @@ import{ArgumentError as k,AuthRequiredError as z,CommandExecutionError as M,Empt
3
3
  return link ? link.getAttribute('href') : null;
4
4
  }`));if(!D||typeof D!=="string")throw new z("x.com","Could not detect logged-in user. Are you logged in?");Z=Q(D);if(!Z)throw new z("x.com","Could not detect logged-in user. Are you logged in?")}await G.goto(`https://x.com/${Z}`);await G.wait(3);const $=(await G.getCookies({url:"https://x.com"})).find((D)=>D.name==="ct0")?.value||null;if(!$)throw new z("x.com","Not logged into x.com (no ct0 cookie)");const L=await b(G,"UserByScreenName",S),X=Y(await G.evaluate(`
5
5
  async () => {
6
- const screenName = "${Z}";
6
+ const screenName = ${JSON.stringify(Z)};
7
7
  const ct0 = ${JSON.stringify($)};
8
8
 
9
9
  const bearer = ${JSON.stringify(x)};
@@ -1,7 +1,8 @@
1
- import{cli as A,Strategy as J}from"@jackwener/opencli/registry";import{AuthRequiredError as W,CliError as F,EmptyResultError as N}from"@jackwener/opencli/errors";import{parseNoteId as O,buildNoteUrl as U}from"./note-helpers.js";const j='.author-wrapper a[href*="/user/profile/"], a.name[href*="/user/profile/"], a.user-name[href*="/user/profile/"], a[href*="/user/profile/"]';export function parseXhsProfileHref(z,B="www.xiaohongshu.com"){const D=typeof z==="string"?z.trim():"";if(!D)return"";const M=String(B||"www.xiaohongshu.com").toLowerCase();let K;try{K=new URL(D,`https://${M}`)}catch{return""}if(K.protocol!=="https:")return"";if(K.hostname.toLowerCase()!==M)return"";return K.pathname.match(/^\/user\/profile\/([a-zA-Z0-9]+)\/?$/)?.[1]??""}export function buildXhsProfileUrl(z,B="www.xiaohongshu.com"){const D=parseXhsProfileHref(z,B);if(!D)return"";return`https://${B}/user/profile/${D}`}export function parseCommentLimit(z,B=20){const D=Number(z);if(!Number.isFinite(D))return B;return Math.max(1,Math.min(Math.floor(D),50))}export function parseXhsLikeCountText(z){const B=/^(?:\d+|\d{1,3}(?:[,,]\d{3})+)\+?$/u,D=/^((?:\d+|\d{1,3}(?:[,,]\d{3})+)(?:\.\d+)?)([wWkK万千])\+?$/u,M=String(z??"").replace(/\s+/g,"");if(!M)return 0;if(B.test(M))return Number(M.replace(/[,+,]/g,""));const K=M.match(D);if(!K)return 0;const V=Number(K[1].replace(/[,,]/g,""));if(!Number.isFinite(V))return 0;const Q=K[2].toLowerCase();return Math.round(V*(Q==="w"||Q==="万"?1e4:1000))}export function buildCommentsExtractJs(z){const B=parseXhsLikeCountText.toString();return`
1
+ import{cli as F,Strategy as U}from"@jackwener/opencli/registry";import{AuthRequiredError as j,CliError as T,CommandExecutionError as Z,EmptyResultError as _}from"@jackwener/opencli/errors";import{parseNoteId as y,buildNoteUrl as P}from"./note-helpers.js";const L='.author-wrapper a[href*="/user/profile/"], a.name[href*="/user/profile/"], a.user-name[href*="/user/profile/"], a[href*="/user/profile/"]';export function parseXhsProfileHref(G,D="www.xiaohongshu.com"){const B=typeof G==="string"?G.trim():"";if(!B)return"";const Q=String(D||"www.xiaohongshu.com").toLowerCase();let K;try{K=new URL(B,`https://${Q}`)}catch{return""}if(K.protocol!=="https:")return"";if(K.hostname.toLowerCase()!==Q)return"";return K.pathname.match(/^\/user\/profile\/([a-zA-Z0-9]+)\/?$/)?.[1]??""}export function buildXhsProfileUrl(G,D="www.xiaohongshu.com"){const B=parseXhsProfileHref(G,D);if(!B)return"";return`https://${D}/user/profile/${B}`}export function parseCommentLimit(G,D=20){const B=Number(G);if(!Number.isFinite(B))return D;return Math.max(1,Math.min(Math.floor(B),50))}export function parseXhsLikeCountText(G){const D=/^(?:\d+|\d{1,3}(?:[,,]\d{3})+)\+?$/u,B=/^((?:\d+|\d{1,3}(?:[,,]\d{3})+)(?:\.\d+)?)([wWkK万千])\+?$/u,Q=String(G??"").replace(/\s+/g,"");if(!Q)return 0;if(D.test(Q))return Number(Q.replace(/[,+,]/g,""));const K=Q.match(B);if(!K)return 0;const M=Number(K[1].replace(/[,,]/g,""));if(!Number.isFinite(M))return 0;const Y=K[2].toLowerCase();return Math.round(M*(Y==="w"||Y==="万"?1e4:1000))}function $(G,D,B){if(G==null)return"";if(typeof G!=="string")throw new Z(`${B}: malformed comment row ${D}`);return G}export function normalizeCommentImages(G,D){if(G==null)return[];if(!Array.isArray(G))throw new Z(`${D}: malformed comment row images`);const B=[];for(const Q of G){if(typeof Q!=="string")throw new Z(`${D}: malformed comment row image URL`);const K=Q.trim();let M;try{M=new URL(K)}catch{throw new Z(`${D}: malformed comment row image URL`)}if(M.protocol!=="https:"&&M.protocol!=="http:"||M.username||M.password)throw new Z(`${D}: malformed comment row image URL`);const Y=M.toString();if(!B.includes(Y))B.push(Y)}return B}export function normalizeCommentRows(G,D="xiaohongshu/comments"){if(G==null)return[];if(!Array.isArray(G))throw new Z(`${D}: malformed comments payload`);return G.map((B,Q)=>{if(!B||typeof B!=="object"||Array.isArray(B))throw new Z(`${D}: malformed comment row at index ${Q}`);const K=$(B.text,"text",D);if(!K)throw new Z(`${D}: malformed comment row text`);const M=Number(B.likes);if(!Number.isInteger(M)||M<0)throw new Z(`${D}: malformed comment row likes`);if(typeof B.is_reply!=="boolean")throw new Z(`${D}: malformed comment row is_reply`);return{author:$(B.author,"author",D),authorHrefRaw:$(B.authorHrefRaw,"authorHrefRaw",D),text:K,likes:M,time:$(B.time,"time",D),is_reply:B.is_reply,reply_to:$(B.reply_to,"reply_to",D),images:normalizeCommentImages(B.images,D)}})}export function buildCommentsExtractJs(G,D=20){const B=parseXhsLikeCountText.toString();return`
2
2
  (async () => {
3
3
  const wait = (ms) => new Promise(r => setTimeout(r, ms))
4
- const withReplies = ${z}
4
+ const withReplies = ${G}
5
+ const targetCount = ${Number(D)||20}
5
6
 
6
7
  // Check login state
7
8
  const bodyText = document.body?.innerText || ''
@@ -9,15 +10,40 @@ import{cli as A,Strategy as J}from"@jackwener/opencli/registry";import{AuthRequi
9
10
  const securityBlock = /安全限制|访问链接异常/.test(bodyText)
10
11
  || /website-login\\/error|error_code=300017|error_code=300031/.test(location.href)
11
12
 
12
- // Scroll the note container to trigger comment loading
13
+ // Scroll to trigger comment loading. Xiaohongshu loads comments in
14
+ // small async batches via IntersectionObserver, and depending on the
15
+ // page layout / viewport the actual scrollable ancestor can be
16
+ // .note-scroller, .container, or the document itself — so each round
17
+ // drives all of them plus scrollIntoView on the last loaded comment,
18
+ // which works regardless of which element actually owns the scrollbar.
19
+ // A single stalled round doesn't mean the list is exhausted — keep
20
+ // going until growth stalls for several consecutive rounds, we've
21
+ // loaded enough top-level comments to satisfy --limit, or we hit the
22
+ // hard round cap.
13
23
  const scroller = document.querySelector('.note-scroller') || document.querySelector('.container')
14
- if (scroller) {
15
- for (let i = 0; i < 3; i++) {
16
- const beforeCount = scroller.querySelectorAll('.parent-comment').length
17
- scroller.scrollTo(0, scroller.scrollHeight)
18
- await wait(800 + Math.random() * 1200)
19
- const afterCount = scroller.querySelectorAll('.parent-comment').length
20
- if (afterCount <= beforeCount) break
24
+ const driveScroll = () => {
25
+ if (scroller) scroller.scrollTo(0, scroller.scrollHeight)
26
+ const comments = document.querySelectorAll('.parent-comment')
27
+ const last = comments[comments.length - 1]
28
+ if (last && typeof last.scrollIntoView === 'function') last.scrollIntoView({ block: 'end' })
29
+ if (typeof window !== 'undefined' && typeof window.scrollTo === 'function') {
30
+ window.scrollTo(0, document.body.scrollHeight)
31
+ }
32
+ }
33
+ {
34
+ let stall = 0
35
+ for (let i = 0; i < 60; i++) {
36
+ const beforeCount = document.querySelectorAll('.parent-comment').length
37
+ if (beforeCount >= targetCount) break
38
+ driveScroll()
39
+ await wait(1000 + Math.random() * 1200)
40
+ const afterCount = document.querySelectorAll('.parent-comment').length
41
+ if (afterCount <= beforeCount) {
42
+ stall++
43
+ if (stall >= 6) break
44
+ } else {
45
+ stall = 0
46
+ }
21
47
  }
22
48
  }
23
49
 
@@ -26,12 +52,27 @@ import{cli as A,Strategy as J}from"@jackwener/opencli/registry";import{AuthRequi
26
52
  const parseLikes = (el) => {
27
53
  return parseLikeCountText(clean(el))
28
54
  }
29
- const HREF_SELECTOR = ${JSON.stringify(j)}
55
+ const HREF_SELECTOR = ${JSON.stringify(L)}
30
56
  const extractAuthorHref = (el) => {
31
57
  if (!el) return ''
32
58
  const anchor = el.querySelector(HREF_SELECTOR)
33
59
  return anchor ? (anchor.getAttribute('href') || '') : ''
34
60
  }
61
+ // Attached comment photos, excluding avatars, inline emoji, badges, and
62
+ // other UI images. Only images inside comment/reply media containers are
63
+ // projected as media evidence.
64
+ const extractImages = (el) => {
65
+ if (!el) return []
66
+ const urls = []
67
+ el.querySelectorAll('img').forEach(img => {
68
+ if (img.classList.contains('avatar-item')) return
69
+ if (img.closest('.content, .note-text')) return
70
+ if (!img.closest('.comment-pic, .reply-pic, .comment-image, .reply-image, .comment-img, .reply-img, [class*="comment-pic"], [class*="reply-pic"], [class*="comment-image"], [class*="reply-image"]')) return
71
+ const src = img.currentSrc || img.src || img.getAttribute('data-src') || ''
72
+ if (src && !urls.includes(src)) urls.push(src)
73
+ })
74
+ return urls
75
+ }
35
76
  const expandReplyThreads = async (root) => {
36
77
  if (!withReplies || !root) return
37
78
  const clickedTexts = new Set()
@@ -65,9 +106,10 @@ import{cli as A,Strategy as J}from"@jackwener/opencli/registry";import{AuthRequi
65
106
  const text = clean(item.querySelector('.content, .note-text'))
66
107
  const likes = parseLikes(item.querySelector('.count'))
67
108
  const time = clean(item.querySelector('.date, .time'))
109
+ const images = extractImages(item)
68
110
 
69
111
  if (!text) continue
70
- results.push({ author, authorHrefRaw, text, likes, time, is_reply: false, reply_to: '' })
112
+ results.push({ author, authorHrefRaw, text, likes, time, is_reply: false, reply_to: '', images })
71
113
 
72
114
  // Extract nested replies (楼中楼)
73
115
  if (withReplies) {
@@ -78,12 +120,13 @@ import{cli as A,Strategy as J}from"@jackwener/opencli/registry";import{AuthRequi
78
120
  const sText = clean(sub.querySelector('.content, .note-text'))
79
121
  const sLikes = parseLikes(sub.querySelector('.count'))
80
122
  const sTime = clean(sub.querySelector('.date, .time'))
123
+ const sImages = extractImages(sub)
81
124
  if (!sText) return
82
- results.push({ author: sAuthor, authorHrefRaw: sAuthorHrefRaw, text: sText, likes: sLikes, time: sTime, is_reply: true, reply_to: author })
125
+ results.push({ author: sAuthor, authorHrefRaw: sAuthorHrefRaw, text: sText, likes: sLikes, time: sTime, is_reply: true, reply_to: author, images: sImages })
83
126
  })
84
127
  }
85
128
  }
86
129
 
87
130
  return { pageUrl: location.href, securityBlock, loginWall, results }
88
131
  })()
89
- `}export const command=A({site:"xiaohongshu",name:"comments",access:"read",description:"获取小红书笔记评论(支持楼中楼子回复)",domain:"www.xiaohongshu.com",strategy:J.COOKIE,navigateBefore:!1,args:[{name:"note-id",required:!0,positional:!0,help:"Full Xiaohongshu note URL with xsec_token"},{name:"limit",type:"int",default:20,help:"Number of top-level comments (max 50)"},{name:"with-replies",type:"boolean",default:!1,help:"Include nested replies (楼中楼)"}],columns:["rank","author","userId","profileUrl","text","likes","time","is_reply","reply_to"],func:async(z,B)=>{const D=parseCommentLimit(B.limit),M=Boolean(B["with-replies"]),K=String(B["note-id"]),V=O(K);await z.goto(U(K,{commandName:"xiaohongshu comments"}));await z.wait({time:2+Math.random()*3});const Q=await z.evaluate(buildCommentsExtractJs(M));if(!Q||typeof Q!=="object")throw new N("xiaohongshu/comments","Unexpected evaluate response");if(Q.securityBlock)throw new F("SECURITY_BLOCK","Xiaohongshu security block: the note detail page was blocked by risk control.",/^https?:\/\//.test(K)?"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(Q.loginWall)throw new W("www.xiaohongshu.com","Note comments require login");const Z=Q.results??[],$=(G,Y)=>({rank:Y+1,author:G.author,userId:G.authorHrefRaw?parseXhsProfileHref(G.authorHrefRaw):"",profileUrl:G.authorHrefRaw?buildXhsProfileUrl(G.authorHrefRaw):"",text:G.text,likes:G.likes,time:G.time,is_reply:G.is_reply,reply_to:G.reply_to});if(M){const G=[];let Y=0;for(const q of Z){if(!q.is_reply)Y++;if(Y>D)break;G.push(q)}return G.map($)}return Z.slice(0,D).map($)}});
132
+ `}export const command=F({site:"xiaohongshu",name:"comments",access:"read",description:"获取小红书笔记评论(支持楼中楼子回复)",domain:"www.xiaohongshu.com",strategy:U.COOKIE,navigateBefore:!1,args:[{name:"note-id",required:!0,positional:!0,help:"Full Xiaohongshu note URL with xsec_token"},{name:"limit",type:"int",default:20,help:"Number of top-level comments (max 50)"},{name:"with-replies",type:"boolean",default:!1,help:"Include nested replies (楼中楼)"}],columns:["rank","author","userId","profileUrl","text","likes","time","is_reply","reply_to","images"],func:async(G,D)=>{const B=parseCommentLimit(D.limit),Q=Boolean(D["with-replies"]),K=String(D["note-id"]),M=y(K);await G.goto(P(K,{commandName:"xiaohongshu comments"}));await G.wait({time:2+Math.random()*3});const Y=await G.evaluate(buildCommentsExtractJs(Q,B));if(!Y||typeof Y!=="object")throw new _("xiaohongshu/comments","Unexpected evaluate response");if(Y.securityBlock)throw new T("SECURITY_BLOCK","Xiaohongshu security block: the note detail page was blocked by risk control.",/^https?:\/\//.test(K)?"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(Y.loginWall)throw new j("www.xiaohongshu.com","Note comments require login");const A=normalizeCommentRows(Y.results,"xiaohongshu/comments"),J=(V,q)=>({rank:q+1,author:V.author,userId:V.authorHrefRaw?parseXhsProfileHref(V.authorHrefRaw):"",profileUrl:V.authorHrefRaw?buildXhsProfileUrl(V.authorHrefRaw):"",text:V.text,likes:V.likes,time:V.time,is_reply:V.is_reply,reply_to:V.reply_to,images:V.images??[]});if(Q){const V=[];let q=0;for(const W of A){if(!W.is_reply)q++;if(q>B)break;V.push(W)}return V.map(J)}return A.slice(0,B).map(J)}});
@@ -0,0 +1,14 @@
1
+ function E(q,D){return q+Math.random()*(D-q)}function V(q,D=0.4,B=0.02){const A=q*D;return Math.max(B,q-A+Math.random()*A*2)}export async function humanWait(q,D,B=0.4){try{await q.wait({time:V(D,B)})}catch{}}export async function humanType(q,D){const B=String(D);if(!B)return;if(typeof q.insertText!=="function"){try{await q.insertText?.(B)}catch{}return}let A=0;while(A<B.length){const H=Math.max(1,Math.round(E(1,4))),I=B.slice(A,A+H);A+=I.length;try{await q.insertText(I)}catch{try{await q.insertText(B.slice(A-I.length))}catch{}return}const K=Math.random()<0.12?E(0.35,0.8):E(0.04,0.18);try{await q.wait({time:K})}catch{}}}function Y(q){if(!q.__xhsCursor)q.__xhsCursor={x:Math.round(E(80,520)),y:Math.round(E(120,420))};return q.__xhsCursor}function Z(q,D,B){const A=D.x-q.x,H=D.y-q.y,I=Math.hypot(A,H)||1,K=-H/I,N=A/I,J=E(0.05,0.22)*I*(Math.random()<0.5?1:-1),Q={x:q.x+A*E(0.2,0.4)+K*J,y:q.y+H*E(0.2,0.4)+N*J},R={x:q.x+A*E(0.6,0.8)+K*J*E(0.3,0.7),y:q.y+H*E(0.6,0.8)+N*J*E(0.3,0.7)},U=[];for(let O=1;O<=B;O++){const F=O/B,G=1-F,W=G*G*G*q.x+3*G*G*F*Q.x+3*G*F*F*R.x+F*F*F*D.x,X=G*G*G*q.y+3*G*G*F*Q.y+3*G*F*F*R.y+F*F*F*D.y;U.push({x:Math.round(W),y:Math.round(X)})}return U}export async function humanMoveTo(q,D,B){if(typeof q.cdp!=="function")return;const A={x:Math.round(D+E(-3,3)),y:Math.round(B+E(-3,3))},H=Y(q),I=Math.hypot(A.x-H.x,A.y-H.y),K=Math.max(6,Math.min(18,Math.round(I/30))),N=Z(H,A,K);for(const J of N){try{await q.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:J.x,y:J.y})}catch{return}try{await q.wait({time:V(0.02,0.6,0.008)})}catch{}}q.__xhsCursor=A}export async function elementViewportCenter(q,D){try{const B=await q.evaluate(`
2
+ (sels => {
3
+ const __opencli_xhs_el_center = true;
4
+ for (const sel of sels) {
5
+ for (const el of document.querySelectorAll(sel)) {
6
+ if (!el || el.offsetParent === null) continue;
7
+ const r = el.getBoundingClientRect();
8
+ if (r.width <= 0 || r.height <= 0) continue;
9
+ return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) };
10
+ }
11
+ }
12
+ return null;
13
+ })(${JSON.stringify(D)})
14
+ `),A=B&&typeof B==="object"&&"data"in B?B.data:B;if(A&&typeof A.x==="number"&&typeof A.y==="number")return A}catch{}return null}
@@ -1,9 +1,9 @@
1
- import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionError as H,ArgumentError as F}from"@jackwener/opencli/errors";import{cli as Vz,Strategy as Hz}from"@jackwener/opencli/registry";import{assertLiteralContent as Gz}from"../_shared/content-guard.js";const Yz="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",x=9,E=20,u=3000,c="|||",N="基础",d=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],Ez=d.map(([z])=>z),R="文字配图",n="再写一张",m="生成图片",i="下一步",_=".tiptap.ProseMirror",Xz=["_onPublish","onPublish","_onSubmit","_handlePublish"],Wz=["_onSave","_onSaveDraft","_onDraft"],A=['[contenteditable="true"][placeholder*="标题"]','[contenteditable="true"][placeholder*="赞"]','input[placeholder*="标题"]','input[placeholder*="title" i]','[contenteditable="true"][class*="title"]','input[maxlength="20"]','input[class*="title"]',".title-input input",".note-title input","input[maxlength]"],l=['[contenteditable="true"][class*="content"]','[contenteditable="true"][class*="editor"]','[contenteditable="true"][placeholder*="描述"]','[contenteditable="true"][placeholder*="正文"]','[contenteditable="true"][placeholder*="内容"]','.note-content [contenteditable="true"]','.editor-content [contenteditable="true"]','[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])'],o={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp"};function W(z){if(z&&typeof z==="object"&&typeof z.session==="string"&&Object.prototype.hasOwnProperty.call(z,"data"))return z.data;return z}function Fz(z){return z.map((J)=>{const K=D.resolve(J);if(!b.existsSync(K))throw new F(`Image file not found: ${K}`);const Z=D.extname(K).toLowerCase();if(!o[Z])throw new F(`Unsupported image format "${Z}". Supported: jpg, png, gif, webp`);return K})}const s='input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]';async function Oz(z,J=15000){const K=500,Z=Math.max(1,Math.ceil(J/K));for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
1
+ import*as _ from"node:fs";import*as D from"node:path";import{CommandExecutionError as V,ArgumentError as F}from"@jackwener/opencli/errors";import{cli as Yz,Strategy as Xz}from"@jackwener/opencli/registry";import{assertLiteralContent as Wz}from"../_shared/content-guard.js";import{humanType as A,humanWait as N,humanMoveTo as Fz,elementViewportCenter as Oz}from"./human.js";const Uz="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",u=9,d=20,n=3000,m="|||",k="基础",i=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],iz=i.map(([z])=>z),T="文字配图",l="再写一张",o="生成图片",s="下一步",S=".tiptap.ProseMirror",vz=["_onPublish","onPublish","_onSubmit","_handlePublish"],Bz=["_onSave","_onSaveDraft","_onDraft"],C=['[contenteditable="true"][placeholder*="标题"]','[contenteditable="true"][placeholder*="赞"]','input[placeholder*="标题"]','input[placeholder*="title" i]','[contenteditable="true"][class*="title"]','input[maxlength="20"]','input[class*="title"]',".title-input input",".note-title input","input[maxlength]"],r=['[contenteditable="true"][class*="content"]','[contenteditable="true"][class*="editor"]','[contenteditable="true"][placeholder*="描述"]','[contenteditable="true"][placeholder*="正文"]','[contenteditable="true"][placeholder*="内容"]','.note-content [contenteditable="true"]','.editor-content [contenteditable="true"]','[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])'],g={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp"};function W(z){if(z&&typeof z==="object"&&typeof z.session==="string"&&Object.prototype.hasOwnProperty.call(z,"data"))return z.data;return z}function Dz(z){return z.map((J)=>{const K=D.resolve(J);if(!_.existsSync(K))throw new F(`Image file not found: ${K}`);const Z=D.extname(K).toLowerCase();if(!g[Z])throw new F(`Unsupported image format "${Z}". Supported: jpg, png, gif, webp`);return K})}const p='input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]';async function fz(z,J=15000){const K=500,Z=Math.max(1,Math.ceil(J/K));for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
2
2
  (() => {
3
- const sels = ${JSON.stringify(s)};
3
+ const sels = ${JSON.stringify(p)};
4
4
  return !!document.querySelector(sels);
5
5
  })()
6
- `))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function r(z,J){if(!await Oz(z))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};if(z.setFileInput)try{await z.setFileInput(J,s);return{ok:!0,count:J.length}}catch($){const j=$ instanceof Error?$.message:String($);if(j.includes("Unknown action")||j.includes("not supported")||j.includes("Not allowed"));else return{ok:!1,count:0,error:j}}const Z=J.map(($)=>{const j=b.readFileSync($).toString("base64"),Y=D.extname($).toLowerCase();return{name:D.basename($),mimeType:o[Y],base64:j}}),Q=Z.reduce(($,j)=>$+j.base64.length,0);if(Q>500000)console.warn(`[warn] Total image payload is ${(Q/1024/1024).toFixed(1)}MB (base64). This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, or compress images before publishing.`);const q=JSON.stringify(Z);return z.evaluate(`
6
+ `))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function t(z,J){if(!await fz(z))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};if(z.setFileInput)try{await z.setFileInput(J,p);return{ok:!0,count:J.length}}catch($){const j=$ instanceof Error?$.message:String($);if(j.includes("Unknown action")||j.includes("not supported")||j.includes("Not allowed"));else return{ok:!1,count:0,error:j}}const Z=J.map(($)=>{const j=_.readFileSync($).toString("base64"),Y=D.extname($).toLowerCase();return{name:D.basename($),mimeType:g[Y],base64:j}}),Q=Z.reduce(($,j)=>$+j.base64.length,0);if(Q>500000)console.warn(`[warn] Total image payload is ${(Q/1024/1024).toFixed(1)}MB (base64). This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, or compress images before publishing.`);const q=JSON.stringify(Z);return z.evaluate(`
7
7
  (async () => {
8
8
  const images = ${q};
9
9
 
@@ -43,11 +43,11 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
43
43
 
44
44
  return { ok: true, count: dt.files.length };
45
45
  })()
46
- `)}async function g(z,J=30000){const K=2000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(!await z.evaluate(`
46
+ `)}async function a(z,J=30000){const K=2000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(!await z.evaluate(`
47
47
  () => !!document.querySelector(
48
48
  '[class*="upload"][class*="progress"], [class*="uploading"], [class*="loading"][class*="image"]'
49
49
  )
50
- `))return;await z.wait({time:K/1000})}}async function p(z,J,K,Z){const Q=await z.evaluate(`
50
+ `))return;await z.wait({time:K/1000})}}async function e(z,J,K,Z){const Q=await z.evaluate(`
51
51
  (function(selectors) {
52
52
  const __opencli_xhs_fill_phase = "locate";
53
53
  for (const sel of selectors) {
@@ -146,7 +146,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
146
146
  fireBeforeInput(el, nextText);
147
147
  return { ok: true };
148
148
  })(${JSON.stringify(Q.sel)}, ${JSON.stringify(K)})
149
- `))?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});throw Error(`Could not prepare ${Z} input. Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}try{await z.insertText(K);$=await z.evaluate(`
149
+ `))?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});throw Error(`Could not prepare ${Z} input. Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}try{await A(z,K);$=await z.evaluate(`
150
150
  ((selector, expectedText) => {
151
151
  const __opencli_xhs_fill_phase = "verify";
152
152
  const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
@@ -169,7 +169,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
169
169
  const actual = normalize(el.innerText || el.textContent || '');
170
170
  return { ok: actual === normalize(expectedText), actual };
171
171
  })(${JSON.stringify(Q.sel)}, ${JSON.stringify(K)})
172
- `)}catch{$=await q()}}else $=await q();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});const j=typeof $?.actual==="string"?$.actual:"";throw Error(`Failed to set ${Z}. Expected "${K}", got "${j}". Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}}async function Uz(z,J){return W(await z.evaluate(`
172
+ `)}catch{$=await q()}}else $=await q();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});const j=typeof $?.actual==="string"?$.actual:"";throw Error(`Failed to set ${Z}. Expected "${K}", got "${j}". Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}}async function Nz(z,J){return W(await z.evaluate(`
173
173
  (selectors => {
174
174
  const el = selectors
175
175
  .map(sel => Array.from(document.querySelectorAll(sel)))
@@ -185,7 +185,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
185
185
  selection?.addRange(range);
186
186
  return true;
187
187
  })(${JSON.stringify(J)})
188
- `))}function uz(z,{click:J=!1}={}){return`
188
+ `))}function lz(z,{click:J=!1}={}){return`
189
189
  (topicName => {
190
190
  const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
191
191
  const want = norm(topicName);
@@ -225,7 +225,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
225
225
  text: (target.innerText || target.textContent || '').trim().slice(0, 40),
226
226
  };
227
227
  })(${JSON.stringify(z)})
228
- `}function cz(z,J){return`
228
+ `}function oz(z,J){return`
229
229
  ((topicName, selectors) => {
230
230
  const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
231
231
  const want = norm(topicName);
@@ -256,7 +256,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
256
256
  }
257
257
  return count;
258
258
  })(${JSON.stringify(z)}, ${JSON.stringify(J)})
259
- `}function t(z,J){return`
259
+ `}function zz(z,J){return`
260
260
  ((topicName, selectors) => {
261
261
  const __opencli_xhs_topic_marker_count = true;
262
262
  const marker = '#' + topicName + '[话题]';
@@ -274,14 +274,14 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
274
274
  }
275
275
  return count;
276
276
  })(${JSON.stringify(z)}, ${JSON.stringify(J)})
277
- `}async function dz(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return W(await z.evaluate(`
277
+ `}async function sz(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return W(await z.evaluate(`
278
278
  (text => {
279
279
  const ok = document.execCommand('insertText', false, text);
280
280
  const active = document.activeElement;
281
281
  if (active) active.dispatchEvent(new Event('input', { bubbles: true }));
282
282
  return ok;
283
283
  })(${JSON.stringify(K)})
284
- `))}async function Bz(z,J,K){const Z=[];for(const Q of K){if(!await Uz(z,J))throw new H(`Could not attach topic "${Q}": body editor not found`);const $=Number(W(await z.evaluate(t(Q,J))))||0;if(typeof z.pressKey==="function")try{await z.pressKey("Enter")}catch{}if(typeof z.insertText!=="function")throw new H(`Could not attach topic "${Q}": page.insertText is unavailable`);try{await z.insertText(`#${Q}`)}catch{throw new H(`Could not attach topic "${Q}": failed to type inline topic query`)}await z.wait({time:1.2});if(typeof z.pressKey!=="function")throw new H(`Could not attach topic "${Q}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch(Y){throw new H(`Could not attach topic "${Q}": failed to accept suggestion (${Y&&Y.message||Y})`)}await z.wait({time:0.8});if((Number(W(await z.evaluate(t(Q,J))))||0)<=$)throw new H(`Could not attach topic "${Q}": no real topic entity appeared after selection`);Z.push(Q);await z.wait({time:0.4})}return Z}async function vz(z){const J=await z.evaluate(`
284
+ `))}async function kz(z,J,K){const Z=[];for(const Q of K){if(!await Nz(z,J))throw new V(`Could not attach topic "${Q}": body editor not found`);const $=Number(W(await z.evaluate(zz(Q,J))))||0;if(typeof z.pressKey==="function")try{await z.pressKey("Enter")}catch{}if(typeof z.insertText!=="function")throw new V(`Could not attach topic "${Q}": page.insertText is unavailable`);try{await A(z,`#${Q}`)}catch{throw new V(`Could not attach topic "${Q}": failed to type inline topic query`)}await N(z,1.2);if(typeof z.pressKey!=="function")throw new V(`Could not attach topic "${Q}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch(Y){throw new V(`Could not attach topic "${Q}": failed to accept suggestion (${Y&&Y.message||Y})`)}await z.wait({time:0.8});if((Number(W(await z.evaluate(zz(Q,J))))||0)<=$)throw new V(`Could not attach topic "${Q}": no real topic entity appeared after selection`);Z.push(Q);await z.wait({time:0.4})}return Z}async function Lz(z){const J=await z.evaluate(`
285
285
  () => {
286
286
  const isVisible = (el) => {
287
287
  if (!el || el.offsetParent === null) return false;
@@ -330,7 +330,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
330
330
  }
331
331
  return { ok: false, visibleTexts };
332
332
  }
333
- `);if(J?.ok)await z.wait({time:1});return J}async function k(z,J,K=7000){const Z=500,Q=Math.max(1,Math.ceil(K/Z));let q={ok:!1};for(let $=0;$<Q;$++){q=W(await z.evaluate(`
333
+ `);if(J?.ok)await z.wait({time:1});return J}async function L(z,J,K=7000){const Z=500,Q=Math.max(1,Math.ceil(K/Z));let q={ok:!1};for(let $=0;$<Q;$++){q=W(await z.evaluate(`
334
334
  ((cfg) => {
335
335
  const __opencli_xhs_click_label = { wantLabel: ${JSON.stringify(J)} };
336
336
  const wantLabel = ${JSON.stringify(J)};
@@ -365,10 +365,10 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
365
365
  }
366
366
  return { ok: false };
367
367
  })()
368
- `));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function Dz(z){const J=await z.evaluate(`
368
+ `));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function Pz(z){const J=await z.evaluate(`
369
369
  (() => {
370
370
  const __opencli_xhs_focus_card = true;
371
- const sel = ${JSON.stringify(_)};
371
+ const sel = ${JSON.stringify(S)};
372
372
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
373
373
  // Prefer the editor inside the active swiper slide if present.
374
374
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
@@ -382,34 +382,34 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
382
382
  selection?.addRange(range);
383
383
  return { ok: true };
384
384
  })()
385
- `);return W(J)}async function fz(z){const J=await z.evaluate(`
385
+ `);return W(J)}async function Iz(z){const J=await z.evaluate(`
386
386
  (() => {
387
387
  const __opencli_xhs_card_text = true;
388
- const sel = ${JSON.stringify(_)};
388
+ const sel = ${JSON.stringify(S)};
389
389
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
390
390
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
391
391
  const text = active ? (active.innerText || active.textContent || '').trim() : '';
392
392
  return { ok: !!text, text };
393
393
  })()
394
- `);return W(J)}async function a(z){const J=await z.evaluate(`
394
+ `);return W(J)}async function Jz(z){const J=await z.evaluate(`
395
395
  (() => {
396
396
  const __opencli_xhs_card_count = true;
397
- const sel = ${JSON.stringify(_)};
397
+ const sel = ${JSON.stringify(S)};
398
398
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
399
399
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
400
400
  const activeText = active ? (active.innerText || active.textContent || '').trim() : '';
401
401
  return { ok: true, count: editors.length, activeEmpty: !activeText };
402
402
  })()
403
- `);return W(J)}async function Nz(z,J=8000){const K=300,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if((await a(z))?.count>=1)return!0;await z.wait({time:K/1000})}return!1}async function kz(z,J,K=6000){const Z=300,Q=Math.ceil(K/Z);for(let q=0;q<Q;q++){const $=await a(z);if($?.count>=J&&$?.activeEmpty)return!0;await z.wait({time:Z/1000})}return!1}async function hz(z,J,K=4){for(let Z=0;Z<K;Z++){if(!(await k(z,n))?.ok)return!1;if(await kz(z,J,2500))return!0}return!1}async function Lz(z){const J=await z.evaluate(`
403
+ `);return W(J)}async function hz(z,J=8000){const K=300,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if((await Jz(z))?.count>=1)return!0;await z.wait({time:K/1000})}return!1}async function Mz(z,J,K=6000){const Z=300,Q=Math.ceil(K/Z);for(let q=0;q<Q;q++){const $=await Jz(z);if($?.count>=J&&$?.activeEmpty)return!0;await z.wait({time:Z/1000})}return!1}async function wz(z,J,K=4){for(let Z=0;Z<K;Z++){if(!(await L(z,l))?.ok)return!1;if(await Mz(z,J,2500))return!0}return!1}async function Rz(z){const J=await z.evaluate(`
404
404
  (() => {
405
405
  const __opencli_xhs_preview_ready = true;
406
406
  const ready = Array.from(document.querySelectorAll('button'))
407
407
  .some((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '下一步');
408
408
  return { ok: ready };
409
409
  })()
410
- `);return W(J)}async function Pz(z,J=6){const K=400,Z=Math.ceil(3000/K);for(let Q=0;Q<J;Q++){if(!(await k(z,m))?.ok&&Q===0)return!1;for(let $=0;$<Z;$++){if((await Lz(z))?.ok)return!0;await z.wait({time:K/1000})}}return!1}async function Iz(z,J,K){J=String(J).replace(/\\n/g,`
411
- `);if(!(await Dz(z))?.ok)throw new H(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const q=J.split(`
412
- `);for(let $=0;$<q.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(q[$])await z.insertText(q[$])}}else await z.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(J)})`);await z.wait({time:0.4});if(!(await fz(z))?.ok)throw new H(`文字配图: card editor #${K+1} is empty after typing`)}async function Mz(z,J){if(!J||J===N)return N;const K=W(await z.evaluate(`
410
+ `);return W(J)}async function _z(z,J=6){const K=400,Z=Math.ceil(3000/K);for(let Q=0;Q<J;Q++){if(!(await L(z,o))?.ok&&Q===0)return!1;for(let $=0;$<Z;$++){if((await Rz(z))?.ok)return!0;await z.wait({time:K/1000})}}return!1}async function bz(z,J,K){J=String(J).replace(/\\n/g,`
411
+ `);if(!(await Pz(z))?.ok)throw new V(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const q=J.split(`
412
+ `);for(let $=0;$<q.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(q[$])await A(z,q[$])}}else await z.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(J)})`);await z.wait({time:0.4});if(!(await Iz(z))?.ok)throw new V(`文字配图: card editor #${K+1} is empty after typing`)}async function yz(z,J){if(!J||J===k)return k;const K=W(await z.evaluate(`
413
413
  (async () => {
414
414
  const __opencli_xhs_card_styles = true;
415
415
  const want = ${JSON.stringify(J)};
@@ -438,7 +438,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
438
438
  }
439
439
  return { ok: seen.length > 0, styles: seen, found: false };
440
440
  })()
441
- `));if(!K?.found)throw new H(`文字配图: requested style "${J}" is not available for this content (options: ${(K?.styles||[]).join(" / ")||"none"}). Choose an available style or omit --card-style to use ${N}.`);if(!(await k(z,J))?.ok)throw new H(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function e(z){const J=await z.evaluate(`
441
+ `));if(!K?.found)throw new V(`文字配图: requested style "${J}" is not available for this content (options: ${(K?.styles||[]).join(" / ")||"none"}). Choose an available style or omit --card-style to use ${k}.`);if(!(await L(z,J))?.ok)throw new V(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function Kz(z){const J=await z.evaluate(`
442
442
  (() => {
443
443
  const __opencli_xhs_composer_media_count = true;
444
444
  const visibleBox = (el) => {
@@ -451,7 +451,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
451
451
  const r = el.getBoundingClientRect();
452
452
  return r.width >= 48 && r.height >= 48;
453
453
  };
454
- const titleSelectors = ${JSON.stringify(A)};
454
+ const titleSelectors = ${JSON.stringify(C)};
455
455
  const titleEl = titleSelectors
456
456
  .map((sel) => Array.from(document.querySelectorAll(sel)))
457
457
  .flat()
@@ -470,7 +470,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
470
470
  }
471
471
  return { ok: true, count };
472
472
  })()
473
- `);return W(J)}async function zz(z,J,K){const Z=await e(z);if(!Z||typeof Z.count!=="number")throw new H(`${K}: could not verify current composer media count`);if(Z.count<J){await z.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new H(`${K}: expected at least ${J} visible media item(s), got ${Z.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function wz(z,J,K){if(!(await k(z,R))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: could not click "${R}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await Nz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: 写文字 card editor did not appear after clicking "${R}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let j=0;j<J.length;j++){if(j>0){if(!await hz(z,j+1)){await z.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new H(`文字配图: new card editor #${j+1} did not render after "${n}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await Iz(z,J[j],j)}if(!await Pz(z)){await z.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new H(`文字配图: "${m}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const q=await Mz(z,K);if(!(await k(z,i))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new H(`文字配图: could not click "${i}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return q}async function Jz(z){return z.evaluate(`
473
+ `);return W(J)}async function Qz(z,J,K){const Z=await Kz(z);if(!Z||typeof Z.count!=="number")throw new V(`${K}: could not verify current composer media count`);if(Z.count<J){await z.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new V(`${K}: expected at least ${J} visible media item(s), got ${Z.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function Az(z,J,K){if(!(await L(z,T))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new V(`文字配图: could not click "${T}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await hz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new V(`文字配图: 写文字 card editor did not appear after clicking "${T}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let j=0;j<J.length;j++){if(j>0){if(!await wz(z,j+1)){await z.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new V(`文字配图: new card editor #${j+1} did not render after "${l}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await bz(z,J[j],j)}if(!await _z(z)){await z.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new V(`文字配图: "${o}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const q=await yz(z,K);if(!(await L(z,s))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new V(`文字配图: could not click "${s}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return q}async function Zz(z){return z.evaluate(`
474
474
  () => {
475
475
  const text = (document.body?.innerText || '').replace(/s+/g, ' ').trim();
476
476
  const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
@@ -500,22 +500,22 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
500
500
  const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
501
501
  return { state, hasTitleInput, hasImageInput, hasVideoSurface };
502
502
  }
503
- `)}async function bz(z,J=5000){const K=500,Z=Math.max(1,Math.ceil(J/K));let Q=await Jz(z);for(let q=0;q<Z;q++){if(Q.state!=="video_surface")return Q;if(q<Z-1){await z.wait({time:K/1000});Q=await Jz(z)}}return Q}async function yz(z,J=1e4){const K=1000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
503
+ `)}async function Tz(z,J=5000){const K=500,Z=Math.max(1,Math.ceil(J/K));let Q=await Zz(z);for(let q=0;q<Z;q++){if(Q.state!=="video_surface")return Q;if(q<Z-1){await z.wait({time:K/1000});Q=await Zz(z)}}return Q}async function Sz(z,J=1e4){const K=1000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
504
504
  (() => {
505
- const sels = ${JSON.stringify(A)};
505
+ const sels = ${JSON.stringify(C)};
506
506
  for (const sel of sels) {
507
507
  const el = document.querySelector(sel);
508
508
  if (el && el.offsetParent !== null) return true;
509
509
  }
510
510
  return false;
511
- })()`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function Rz(z){const J=W(await z.evaluate(`
511
+ })()`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function Cz(z){const J=W(await z.evaluate(`
512
512
  (() => {
513
513
  const __opencli_xhs_restore_srcs = true;
514
514
  return Array.from(document.querySelectorAll('.img-container img'))
515
515
  .map((img) => img.currentSrc || img.src || '')
516
516
  .filter(Boolean);
517
517
  })()
518
- `));return Array.isArray(J)?J:[]}async function Kz(z,J){if(!Array.isArray(J)||J.length===0)return 0;const K=W(await z.evaluate(`
518
+ `));return Array.isArray(J)?J:[]}async function $z(z,J){if(!Array.isArray(J)||J.length===0)return 0;const K=W(await z.evaluate(`
519
519
  (async (wanted) => {
520
520
  const __opencli_xhs_delete_by_src = true;
521
521
  const want = new Set(wanted);
@@ -546,7 +546,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
546
546
  }
547
547
  return removed;
548
548
  })(${JSON.stringify(J)})
549
- `));return Number(K)||0}function _z(z){const J=String(z&&(z.url||z.key)||""),K=String(z&&z.method||"").toUpperCase(),Z=Number(z&&z.status||0);if(K!=="PUT")return null;if(!/ros-upload[^/]*\/spectrum\//.test(J))return null;if(Z<200||Z>=300)return null;const Q=J.match(/\/spectrum\/([^?#]+)/);return Q?Q[1]:J}function Qz(z){return Math.min(45,6+z*4)}async function Zz(z,J,K,Z=90000){if(J<=0)return{ok:!0,completed:0};if(!K||typeof z.readNetworkCapture!=="function"){await z.wait({time:Qz(J)});return{ok:!0,completed:null,viaFallback:!0}}const Q=1500,$=Math.ceil(9000/Q),j=Math.ceil(Z/Q),Y=new Set;for(let U=0;U<j;U++){const f=await z.readNetworkCapture().catch(()=>[]);for(const B of Array.isArray(f)?f:[]){const G=_z(B);if(G)Y.add(G)}if(Y.size>=J)return{ok:!0,completed:Y.size};if(U>=$&&Y.size===0){await z.wait({time:Qz(J)});return{ok:!0,completed:null,viaFallback:!0}}await z.wait({time:Q/1000})}return{ok:!1,completed:Y.size}}Vz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Hz.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${c} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${N}。可选: ${d.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Z=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(Z){if(Q)throw new F("正文位置参数和 --file 只能二选一,不能同时给");const V=D.resolve(Z);if(!b.existsSync(V))throw new F(`--file 指向的文件不存在: ${V}`);Q=b.readFileSync(V,"utf-8").trim();if(!Q)throw new F(`--file 指向的文件内容为空: ${V}`)}const q=J.images?String(J.images).split(",").map((V)=>V.trim()).filter(Boolean):[],$=J.topics?String(J.topics).split(",").map((V)=>V.trim()).filter(Boolean):[],j=Boolean(J.draft),Y=J["card-text"]?String(J["card-text"]):"",U=Y?Y.split(c).map((V)=>V.trim()).filter(Boolean):[],f=J["card-style"]?String(J["card-style"]).trim():"",B=U.length>0;if(!K)throw new F("--title is required");if(K.length>E)throw new F(`Title is ${K.length} chars — must be ≤ ${E}`);if(!Q)throw new F("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");Gz(Q,{fileFlag:"--file"});if(!B&&q.length===0)throw new F("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>x)throw new F(`Too many images: ${q.length} (max ${x})`);if(B&&q.some((V)=>D.extname(V).toLowerCase()===".gif"))throw new F("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const G=Fz(q);await z.goto(Yz);await z.wait({time:1});await z.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let h="";for(let V=0;V<30;V++){await z.wait({time:0.5});h=await z.evaluate("() => location.href");if(h.includes("creator.xiaohongshu.com"))break}if(!h.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${h}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const L=await vz(z);if((await bz(z,L?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const V=L?.ok?`clicked "${L.text}"`:`visible candidates: ${(L?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${V}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const P=await Rz(z);let I=f;if(B)I=await wz(z,U,f);else{const V=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await r(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`图片注入失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await z.wait({time:u/1000});const v=await Zz(z,G.length,V);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`图片上传未全部完成:只确认 ${v.completed}/${G.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await g(z);if(P.length){await Kz(z,P);await z.wait({time:0.5})}const w=await e(z);if(w&&typeof w.count==="number"&&w.count>G.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new H(`清掉恢复态旧图后编辑器内仍有 ${w.count} 张图,超过本次注入的 ${G.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await yz(z)){await z.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new H("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(B)await zz(z,U.length,"文字配图 generated images");if(B&&G.length>0){const V=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await r(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`追加图片失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await z.wait({time:u/1000});const v=await Zz(z,G.length,V);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`追加图片上传未全部完成:只确认 ${v.completed}/${G.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await g(z);await zz(z,U.length+G.length,"文字配图 appended images")}if(B&&P.length){await Kz(z,P);await z.wait({time:0.5})}await p(z,A,K,"title");await z.wait({time:0.5});await p(z,l,Q,"content");await z.wait({time:0.5});let y=[];if($.length)y=await Bz(z,l,$);const T=j?["暂存离开","存草稿"]:["发布","发布笔记"],X=await z.evaluate(`
549
+ `));return Number(K)||0}function xz(z){const J=String(z&&(z.url||z.key)||""),K=String(z&&z.method||"").toUpperCase(),Z=Number(z&&z.status||0);if(K!=="PUT")return null;if(!/ros-upload[^/]*\/spectrum\//.test(J))return null;if(Z<200||Z>=300)return null;const Q=J.match(/\/spectrum\/([^?#]+)/);return Q?Q[1]:J}function qz(z){return Math.min(45,6+z*4)}async function jz(z,J,K,Z=90000){if(J<=0)return{ok:!0,completed:0};if(!K||typeof z.readNetworkCapture!=="function"){await z.wait({time:qz(J)});return{ok:!0,completed:null,viaFallback:!0}}const Q=1500,$=Math.ceil(9000/Q),j=Math.ceil(Z/Q),Y=new Set;for(let U=0;U<j;U++){const f=await z.readNetworkCapture().catch(()=>[]);for(const v of Array.isArray(f)?f:[]){const G=xz(v);if(G)Y.add(G)}if(Y.size>=J)return{ok:!0,completed:Y.size};if(U>=$&&Y.size===0){await z.wait({time:qz(J)});return{ok:!0,completed:null,viaFallback:!0}}await z.wait({time:Q/1000})}return{ok:!1,completed:Y.size}}Yz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Xz.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${m} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${k}。可选: ${i.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Z=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(Z){if(Q)throw new F("正文位置参数和 --file 只能二选一,不能同时给");const H=D.resolve(Z);if(!_.existsSync(H))throw new F(`--file 指向的文件不存在: ${H}`);Q=_.readFileSync(H,"utf-8").trim();if(!Q)throw new F(`--file 指向的文件内容为空: ${H}`)}const q=J.images?String(J.images).split(",").map((H)=>H.trim()).filter(Boolean):[],$=J.topics?String(J.topics).split(",").map((H)=>H.trim()).filter(Boolean):[],j=Boolean(J.draft),Y=J["card-text"]?String(J["card-text"]):"",U=Y?Y.split(m).map((H)=>H.trim()).filter(Boolean):[],f=J["card-style"]?String(J["card-style"]).trim():"",v=U.length>0;if(!K)throw new F("--title is required");if(K.length>d)throw new F(`Title is ${K.length} chars — must be ≤ ${d}`);if(!Q)throw new F("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");Wz(Q,{fileFlag:"--file"});if(!v&&q.length===0)throw new F("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>u)throw new F(`Too many images: ${q.length} (max ${u})`);if(v&&q.some((H)=>D.extname(H).toLowerCase()===".gif"))throw new F("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const G=Dz(q);await z.goto(Uz);await z.wait({time:1});await z.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let P="";for(let H=0;H<30;H++){await z.wait({time:0.5});P=await z.evaluate("() => location.href");if(P.includes("creator.xiaohongshu.com"))break}if(!P.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${P}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const I=await Lz(z);if((await Tz(z,I?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const H=I?.ok?`clicked "${I.text}"`:`visible candidates: ${(I?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${H}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const h=await Cz(z);let M=f;if(v)M=await Az(z,U,f);else{const H=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await t(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new V(`图片注入失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await z.wait({time:n/1000});const B=await jz(z,G.length,H);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new V(`图片上传未全部完成:只确认 ${B.completed}/${G.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await a(z);if(h.length){await $z(z,h);await z.wait({time:0.5})}const R=await Kz(z);if(R&&typeof R.count==="number"&&R.count>G.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new V(`清掉恢复态旧图后编辑器内仍有 ${R.count} 张图,超过本次注入的 ${G.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await Sz(z)){await z.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new V("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(v)await Qz(z,U.length,"文字配图 generated images");if(v&&G.length>0){const H=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await t(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new V(`追加图片失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await z.wait({time:n/1000});const B=await jz(z,G.length,H);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new V(`追加图片上传未全部完成:只确认 ${B.completed}/${G.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await a(z);await Qz(z,U.length+G.length,"文字配图 appended images")}if(v&&h.length){await $z(z,h);await z.wait({time:0.5})}await e(z,C,K,"title");await N(z,0.6);await e(z,r,Q,"content");await N(z,0.6);let b=[];if($.length)b=await kz(z,r,$);await N(z,1,0.6);const y=await Oz(z,["xhs-publish-btn"]);if(y){await Fz(z,y.x,y.y);await N(z,0.4)}const x=j?["暂存离开","存草稿"]:["发布","发布笔记"],X=await z.evaluate(`
550
550
  (cfg => {
551
551
  const { isDraftMode, publishNames, draftNames, labels } = cfg;
552
552
  const isVisible = (el) => {
@@ -585,7 +585,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
585
585
  }
586
586
  }
587
587
  return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
588
- })(${JSON.stringify({isDraftMode:j,publishNames:Xz,draftNames:Wz,labels:T})})
588
+ })(${JSON.stringify({isDraftMode:j,publishNames:vz,draftNames:Bz,labels:x})})
589
589
  `);if(!X?.ok){if(j){if(await z.evaluate(`
590
590
  (() => {
591
591
  const labels = ['返回', '关闭', '取消', '离开'];
@@ -624,7 +624,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
624
624
  }
625
625
  return false;
626
626
  }
627
- `)){X.ok=!0;X.via="auto-save"}}}}if(!X?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const V=X?.via?` (via=${X.via})`:"",O=X?.error?`, error=${X.error}`:"",v=X?.lastMethodError?`, lastMethodError=${X.lastMethodError}`:"";throw Error(`未能触发「${T[0]}」按钮${V}${O}${v}。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_submit_debug.png")}await z.wait({time:4});const M=await z.evaluate("() => location.href"),$z=j?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],S=await z.evaluate(`
627
+ `)){X.ok=!0;X.via="auto-save"}}}}if(!X?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const H=X?.via?` (via=${X.via})`:"",O=X?.error?`, error=${X.error}`:"",B=X?.lastMethodError?`, lastMethodError=${X.lastMethodError}`:"";throw Error(`未能触发「${x[0]}」按钮${H}${O}${B}。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_submit_debug.png")}await z.wait({time:4});const w=await z.evaluate("() => location.href"),Hz=j?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],E=await z.evaluate(`
628
628
  (markers => {
629
629
  for (const el of document.querySelectorAll('*')) {
630
630
  if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
@@ -633,5 +633,5 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
633
633
  if (el.children.length === 0 && markers.some(marker => text.includes(marker))) return text;
634
634
  }
635
635
  return '';
636
- })(${JSON.stringify($z)})
637
- `),qz=!M.includes("/publish/publish"),jz=S.length>0||qz,C=j?"暂存成功":"发布成功";if(!jz)throw new H(`${C}无法确认:未观察到成功提示或提交后跳转。`+(M?`当前 URL: ${M}。`:"当前 URL 为空。")+"若确属失败,直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。");return[{status:`✅ ${C}`,detail:[`"${K}"`,B?`${U.length}张文字配图${G.length?` + ${G.length}张图片`:""}${I&&I!==N?` (${I})`:""}`:`${G.length}张图片`,y.length?`话题: ${y.join(" ")}`:"",S||M||""].filter(Boolean).join(" · ")}]}});
636
+ })(${JSON.stringify(Hz)})
637
+ `),Vz=!w.includes("/publish/publish"),Gz=E.length>0||Vz,c=j?"暂存成功":"发布成功";if(!Gz)throw new V(`${c}无法确认:未观察到成功提示或提交后跳转。`+(w?`当前 URL: ${w}。`:"当前 URL 为空。")+"若确属失败,直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。");return[{status:`✅ ${c}`,detail:[`"${K}"`,v?`${U.length}张文字配图${G.length?` + ${G.length}张图片`:""}${M&&M!==k?` (${M})`:""}`:`${G.length}张图片`,b.length?`话题: ${b.join(" ")}`:"",E||w||""].filter(Boolean).join(" · ")}]}});
@@ -1,4 +1,4 @@
1
- import{ArgumentError as V,AuthRequiredError as F,CommandExecutionError as K,EmptyResultError as W}from"@jackwener/opencli/errors";export const MAX_LIMIT=1000;export function validateLimit(G,B=20){const J=G??B;let D;if(typeof J==="number")D=J;else{const H=String(J).trim();if(!/^\d+$/.test(H))throw new V(`--limit must be a positive integer no greater than ${MAX_LIMIT}`,"Use a normal-sized decimal integer limit to avoid slow requests or Zhihu risk controls");D=Number(H)}if(!Number.isInteger(D)||D<=0||D>MAX_LIMIT)throw new V(`--limit must be a positive integer no greater than ${MAX_LIMIT}`,"Use a normal-sized decimal integer limit to avoid slow requests or Zhihu risk controls");return D}export function unwrapEvaluateResult(G){if(G&&typeof G==="object"&&"data"in G&&"session"in G)return G.data;return G}export function requireZhihuListPayload(G,B,J){const D=unwrapEvaluateResult(G);if(!D||typeof D!=="object"||Array.isArray(D))throw new K(`Zhihu ${B} returned malformed payload`,`URL: ${J}`);if(D.__httpError){const H=D.__httpError;if(H===401||H===403)throw new F("www.zhihu.com",`Failed to fetch Zhihu ${B}`);if(H===404)throw new W(`zhihu ${B}`,"Check the target identifier");throw new K(`Zhihu ${B} request failed${H?` (HTTP ${H})`:""}`,"Try again later or rerun with -v")}if(D.__fetchError)throw new K(`Zhihu ${B} request failed`,String(D.__fetchError));if(!Array.isArray(D.data))throw new K(`Zhihu ${B} returned malformed data list`,`URL: ${J}`);if(!D.paging||typeof D.paging!=="object")throw new K(`Zhihu ${B} returned malformed paging data`,`URL: ${J}`);return D}function Y(G){if(typeof G!=="string"||!G)return"";try{const B=new URL(G);if(B.protocol!=="https:")return"";if(B.hostname==="api.zhihu.com"&&B.pathname.startsWith("/members/"))return`https://www.zhihu.com/api/v4${B.pathname}${B.search}`;if(B.hostname==="www.zhihu.com"&&B.pathname.startsWith("/api/v4/members/"))return B.toString()}catch{return""}return""}function $(G,B){try{return new URL(G).pathname===new URL(B).pathname}catch{return!1}}export async function fetchZhihuList(G,B,J,D){const H=[],Q=new Set;let N=B;while(N&&H.length<J&&!Q.has(N)){Q.add(N);const S=requireZhihuListPayload(await G.evaluate(`
1
+ import{ArgumentError as V,AuthRequiredError as F,CommandExecutionError as K,EmptyResultError as W}from"@jackwener/opencli/errors";export const MAX_LIMIT=1000;export function validateLimit(G,B=20){const J=G??B;let D;if(typeof J==="number")D=J;else{const H=String(J).trim();if(!/^\d+$/.test(H))throw new V(`--limit must be a positive integer no greater than ${MAX_LIMIT}`,"Use a normal-sized decimal integer limit to avoid slow requests or Zhihu risk controls");D=Number(H)}if(!Number.isInteger(D)||D<=0||D>MAX_LIMIT)throw new V(`--limit must be a positive integer no greater than ${MAX_LIMIT}`,"Use a normal-sized decimal integer limit to avoid slow requests or Zhihu risk controls");return D}export function unwrapEvaluateResult(G){if(G&&typeof G==="object"&&"data"in G&&"session"in G)return G.data;return G}export function requireZhihuListPayload(G,B,J){const D=unwrapEvaluateResult(G);if(!D||typeof D!=="object"||Array.isArray(D))throw new K(`Zhihu ${B} returned malformed payload`,`URL: ${J}`);if(D.__httpError){const H=D.__httpError;if(H===401||H===403)throw new F("www.zhihu.com",`Failed to fetch Zhihu ${B}`);if(H===404)throw new W(`zhihu ${B}`,"Check the target identifier");throw new K(`Zhihu ${B} request failed${H?` (HTTP ${H})`:""}`,"Try again later or rerun with -v")}if(D.__fetchError)throw new K(`Zhihu ${B} request failed`,String(D.__fetchError));if(!Array.isArray(D.data))throw new K(`Zhihu ${B} returned malformed data list`,`URL: ${J}`);if(!D.paging||typeof D.paging!=="object")throw new K(`Zhihu ${B} returned malformed paging data`,`URL: ${J}`);return D}function Y(G){if(typeof G!=="string"||!G)return"";try{const B=new URL(G);if(B.protocol!=="https:"&&B.protocol!=="http:")return"";B.protocol="https:";if(B.hostname==="api.zhihu.com"&&B.pathname.startsWith("/members/"))return`https://www.zhihu.com/api/v4${B.pathname}${B.search}`;if(B.hostname==="www.zhihu.com"&&B.pathname.startsWith("/api/v4/members/"))return B.toString()}catch{return""}return""}function $(G,B){try{return new URL(G).pathname===new URL(B).pathname}catch{return!1}}export async function fetchZhihuList(G,B,J,D){const H=[],Q=new Set;let N=B;while(N&&H.length<J&&!Q.has(N)){Q.add(N);const S=requireZhihuListPayload(await G.evaluate(`
2
2
  (async () => {
3
3
  try {
4
4
  const r = await fetch(${JSON.stringify(N)}, { credentials: 'include' });
@@ -8,4 +8,4 @@ import{ArgumentError as V,AuthRequiredError as F,CommandExecutionError as K,Empt
8
8
  return { __fetchError: err?.message || String(err) };
9
9
  }
10
10
  })()
11
- `),D,N);for(const j of S.data){H.push(j);if(H.length>=J)break}if(S.paging?.is_end)break;const O=Y(S.paging?.next);if(!O||!$(O,B))throw new K(`Zhihu ${D} pagination returned malformed next URL`);if(Q.has(O))throw new K(`Zhihu ${D} pagination returned a repeated next URL`);N=O}if(H.length===0)throw new W(`zhihu ${D}`,"No rows were returned for the requested Zhihu user.");return H}export const __test__={normalizeZhihuApiUrl:Y,sameZhihuApiPath:$};
11
+ `),D,N);for(const j of S.data){H.push(j);if(H.length>=J)break}if(H.length>=J)break;if(S.paging?.is_end)break;const O=Y(S.paging?.next);if(!O||!$(O,B))throw new K(`Zhihu ${D} pagination returned malformed next URL`);if(Q.has(O))throw new K(`Zhihu ${D} pagination returned a repeated next URL`);N=O}if(H.length===0)throw new W(`zhihu ${D}`,"No rows were returned for the requested Zhihu user.");return H}export const __test__={normalizeZhihuApiUrl:Y,sameZhihuApiPath:$};
@@ -1 +1 @@
1
- import{execFileSync as D}from"node:child_process";import*as B from"node:fs";import*as q from"node:path";import{request as E}from"node:http";import{WebSocket as L}from"ws";import{probeCDP as I,launchDetachedApp as P}from"../launcher.js";import{withBrowserSessionLock as f}from"../browser-session-lock.js";import{CommandExecutionError as S}from"../errors.js";import{log as U}from"../logger.js";import{addManagedProfile as w,getManagedProfile as R,listManagedProfiles as u,managedProfilesBaseDir as C,removeManagedProfileEntry as y,updateManagedProfile as g}from"./profile.js";import{proxyServerFlag as m}from"./proxy-test.js";import{resolveFingerprint as h,randomSeed as l}from"./fingerprint.js";const c=500,_=20000,j=9301,v=9399;function A(K){for(const Q of K)try{B.accessSync(Q,B.constants.X_OK);return Q}catch{}return null}function d(K){for(const Q of K)try{const Y=D("which",[Q],{encoding:"utf-8",stdio:"pipe"}).trim();if(Y)return Y}catch{}return null}export function discoverChromePath(){const K=process.env.OPENCLI_CHROME_PATH?.trim();if(K)return K;if(process.platform==="darwin")return A(["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]);if(process.platform==="win32"){const Q=process.env.ProgramFiles??"C:\\Program Files",Y=process.env["ProgramFiles(x86)"]??"C:\\Program Files (x86)",Z=process.env.LOCALAPPDATA??"";return A([q.join(Q,"Google","Chrome","Application","chrome.exe"),q.join(Y,"Google","Chrome","Application","chrome.exe"),...Z?[q.join(Z,"Google","Chrome","Application","chrome.exe")]:[],q.join(Q,"Microsoft","Edge","Application","msedge.exe"),q.join(Y,"Microsoft","Edge","Application","msedge.exe")])}return d(["google-chrome-stable","google-chrome","chromium","chromium-browser","microsoft-edge-stable"])}function F(K,Q,Y=5000){return new Promise((Z,$)=>{const X=new URL(Q),H=E({hostname:X.hostname,port:X.port,path:X.pathname+X.search,method:K,timeout:Y},(V)=>{const J=[];V.on("data",(z)=>J.push(Buffer.isBuffer(z)?z:Buffer.from(z)));V.on("end",()=>{const z=Buffer.concat(J).toString("utf8");if((V.statusCode??0)<200||(V.statusCode??0)>=300){$(Error(`HTTP ${V.statusCode}: ${z.slice(0,200)}`));return}try{Z(z?JSON.parse(z):null)}catch{Z(z)}})});H.on("error",$);H.on("timeout",()=>H.destroy(Error(`请求 ${Q} 超时`)));H.end()})}export function managedEndpoint(K){return`http://127.0.0.1:${K.port}`}export async function isManagedChromeRunning(K){return I(K.port)}const i=["--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-domain-reliability","--disable-breakpad","--disable-client-side-phishing-detection","--no-pings","--no-service-autorun","--password-store=basic","--disable-features=Translate,OptimizationHints,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions,CalculateNativeWinOcclusion"];function p(K){const Q=[`--user-data-dir=${K.userDataDir}`,`--remote-debugging-port=${K.port}`,"--remote-allow-origins=*"];if(K.clean!==!1)Q.push(...i);if(K.webrtc!==!1)Q.push("--force-webrtc-ip-handling-policy=disable_non_proxied_udp");if(K.proxy)Q.push(`--proxy-server=${m(K.proxy)}`);if(K.fingerprint?.enabled&&K.fingerprint.locale)Q.push(`--lang=${K.fingerprint.locale}`);Q.push("--no-startup-window");return Q}function o(K){const Q=K.fingerprint?.enabled?K.fingerprint.timezone:void 0;return Q?{TZ:Q}:void 0}async function s(K,Q){const Y=Date.now()+_;while(Date.now()<Y){if(await I(K,1000))return;await new Promise((Z)=>setTimeout(Z,c))}throw new S(`托管 Chrome「${Q}」已启动但 ${_/1000}s 内 CDP 端口 ${K} 未就绪`,"浏览器可能启动缓慢,稍后重试;或检查该端口是否被其它进程占用。")}const a="https://publishport.app/automation";async function b(K,Q){const Z=(await F("GET",`${K}/json/version`))?.webSocketDebuggerUrl;if(!Z)throw Error(`CDP 端点 ${K} 没有暴露 browser target`);const $=new L(Z);await new Promise((J,z)=>{const W=setTimeout(()=>{$.close();z(Error("browser CDP 连接超时"))},1e4);$.on("open",()=>{clearTimeout(W);J()});$.on("error",(G)=>{clearTimeout(W);z(G)})});let X=0;const H=new Map;$.on("message",(J)=>{try{const z=JSON.parse(String(J));if(!z.id||!H.has(z.id))return;const W=H.get(z.id);H.delete(z.id);if(z.error)W.reject(Error(z.error.message??"CDP error"));else W.resolve(z.result??{})}catch{}});const V=(J,z={})=>{const W=++X;return new Promise((G,T)=>{const k=setTimeout(()=>{H.delete(W);T(Error(`CDP ${J} 超时`))},1e4);H.set(W,{resolve:(N)=>{clearTimeout(k);G(N)},reject:(N)=>{clearTimeout(k);T(N)}});$.send(JSON.stringify({id:W,method:J,params:z}))})};try{return await Q(V)}finally{$.close()}}async function x(K,Q){try{const Y=await F("GET",`${K}/json`),Z=Array.isArray(Y)?Y.find(($)=>$.type==="page"):void 0;if(!Z)await b(K,async($)=>{const H=(await $("Target.createTarget",{url:a,background:!0})).targetId;if(H&&Q==="background"){const V=await $("Browser.getWindowForTarget",{targetId:H});if(V.windowId!==void 0)for(let J=0;J<4;J++){await $("Browser.setWindowBounds",{windowId:V.windowId,bounds:{windowState:"minimized"}});await new Promise((W)=>setTimeout(W,250));if((await $("Browser.getWindowBounds",{windowId:V.windowId})).bounds?.windowState==="minimized")break}}});else if(Q==="foreground"&&Z.id)await b(K,async($)=>{const X=await $("Browser.getWindowForTarget",{targetId:Z.id});if((await $("Browser.getWindowBounds",{windowId:X.windowId})).bounds?.windowState==="minimized")await $("Browser.setWindowBounds",{windowId:X.windowId,bounds:{windowState:"normal"}})})}catch(Y){U.debug(`[managed-chrome] ensurePageTarget 失败(非致命):${Y instanceof Error?Y.message:Y}`)}}const O=new Map;async function M(K){if(await I(K.port))return!0;if(process.platform==="win32")return!1;const Q=q.join(K.userDataDir,"SingletonLock"),Y=()=>{try{B.lstatSync(Q);return!0}catch{return!1}};if(!Y())return!1;for(let Z=0;Z<3;Z++){await new Promise(($)=>setTimeout($,500));if(await I(K.port,4000))return!0;if(!Y())return!1}return!1}export async function ensureManagedChrome(K,Q,Y={}){const Z=managedEndpoint(Q),$=O.get(K);if($)return $;if(await M(Q)){U.debug(`[managed-chrome] profile「${K}」已在端口 ${Q.port} 运行`);await x(Z,Y.windowMode);return Z}const X=O.get(K);if(X)return X;const H=f(async()=>{if(await M(Q)){U.debug(`[managed-chrome] profile「${K}」等锁期间已被拉起,直接复用`);await x(Z,Y.windowMode);return Z}const V=Q.browserPath??discoverChromePath();if(!V)throw new S("找不到可用的 Chrome/Chromium 浏览器。","安装 Google Chrome,或用 OPENCLI_CHROME_PATH 环境变量指定可执行文件路径。");B.mkdirSync(Q.userDataDir,{recursive:!0});U.debug(`[managed-chrome] 拉起 profile「${K}」:${V} 端口 ${Q.port}${Q.proxy?" (代理)":""}`);await P(V,p(Q),`托管 profile ${K}`,o(Q));await s(Q.port,K);await x(Z,Y.windowMode);return Z},`launch-${K}`).finally(()=>O.delete(K));O.set(K,H);return H}export async function stopManagedChrome(K){if(!await I(K.port))return!1;const Y=(await F("GET",`${managedEndpoint(K)}/json/version`))?.webSocketDebuggerUrl;if(!Y)throw Error(`端口 ${K.port} 上的 CDP 端点没有暴露 browser target`);await new Promise(($,X)=>{const H=new L(Y),V=setTimeout(()=>{H.close();X(Error("CDP Browser.close 超时"))},1e4);H.on("open",()=>{H.send(JSON.stringify({id:1,method:"Browser.close",params:{}}))});H.on("close",()=>{clearTimeout(V);$()});H.on("error",(J)=>{clearTimeout(V);X(J)})});const Z=Date.now()+5000;while(Date.now()<Z){if(!await I(K.port,500))break;await new Promise(($)=>setTimeout($,200))}if(process.platform!=="win32"){const $=q.join(K.userDataDir,"SingletonLock"),X=Date.now()+15000;while(Date.now()<X){try{B.lstatSync($)}catch{return!0}await new Promise((H)=>setTimeout(H,200))}U.warn(`[managed-chrome] Chrome 端口已释放但 15s 内 SingletonLock 未消失:${$}`)}return!0}async function n(K){return!await I(K,500)}async function t(){const K=new Set(u().map(({entry:Q})=>Q.port));for(let Q=j;Q<=v;Q++){if(K.has(Q))continue;if(await n(Q))return Q}throw Error(`托管 profile 端口区间 ${j}-${v} 已耗尽`)}export async function resolveManagedOpts(K){const Q={};if(K.label)Q.label=K.label;if(K.proxy)Q.proxy=K.proxy;if(K.webrtc===!1)Q.webrtc=!1;if(K.clean===!1)Q.clean=!1;if(K.fingerprint?.enabled){const Y=K.fingerprint,Z=Y.seed??l();Q.fingerprint=h(Z,{timezone:Y.timezone,locale:Y.locale,userAgent:Y.userAgent,platform:Y.platform,spoof:Y.spoof,hardwareConcurrency:Y.hardwareConcurrency,deviceMemory:Y.deviceMemory,screen:Y.screen,webglVendor:Y.webglVendor,webglRenderer:Y.webglRenderer,canvasNoise:Y.canvasNoise})}return{opts:Q}}export async function createManagedProfile(K,Q={}){const Y=await t(),Z=q.join(C(),K.trim()),$={userDataDir:Z,port:Y,...Q.browserPath?{browserPath:Q.browserPath}:{},...Q.label?{label:Q.label}:{},...Q.proxy?{proxy:Q.proxy}:{},...Q.fingerprint?{fingerprint:Q.fingerprint}:{},...Q.webrtc===!1?{webrtc:!1}:{},...Q.clean===!1?{clean:!1}:{},createdAt:new Date().toISOString()};w(K,$);B.rmSync(Z,{recursive:!0,force:!0});B.mkdirSync(Z,{recursive:!0});return $}export async function updateManagedProfileConfig(K,Q){const Y=R(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);const Z=Q.webrtc!==void 0?Q.webrtc:Y.webrtc,$=Q.clean!==void 0?Q.clean:Y.clean,X={userDataDir:Y.userDataDir,port:Y.port,...Y.browserPath?{browserPath:Y.browserPath}:{},...Y.createdAt?{createdAt:Y.createdAt}:{},...Q.label!==void 0?Q.label?{label:Q.label}:{}:Y.label?{label:Y.label}:{},...Q.proxy!==void 0?Q.proxy?{proxy:Q.proxy}:{}:Y.proxy?{proxy:Y.proxy}:{},...Q.fingerprint!==void 0?Q.fingerprint?{fingerprint:Q.fingerprint}:{}:Y.fingerprint?{fingerprint:Y.fingerprint}:{},...Z===!1?{webrtc:!1}:{},...$===!1?{clean:!1}:{}};let H=!1;try{H=await stopManagedChrome(Y)}catch(V){U.warn(`[managed-chrome] 更新前关闭 profile「${K}」失败(继续):${V instanceof Error?V.message:V}`)}g(K,X);return{entry:X,wasRunning:H}}async function r(K){const Q=Date.now()+15000;let Y=0;while(Date.now()<Q){try{B.rmSync(K,{recursive:!0,force:!0,maxRetries:3,retryDelay:100})}catch{}await new Promise((Z)=>setTimeout(Z,300));if(!B.existsSync(K)){if(++Y>=2)return}else Y=0}throw new S(`数据目录未能删净(浏览器可能仍在退出中):${K}`,"等浏览器窗口完全关闭后再删一次即可。")}export async function removeManagedProfile(K,Q={}){const Y=R(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);try{await stopManagedChrome(Y)}catch(Z){U.warn(`[managed-chrome] 关闭 profile「${K}」失败(继续删除):${Z instanceof Error?Z.message:Z}`)}if(!Q.keepData){const Z=C(),$=q.resolve(Y.userDataDir);if($.startsWith(Z+q.sep))await r($);else U.warn(`[managed-chrome] user-data-dir 不在 ${Z} 下,跳过数据删除:${$}`)}y(K)}
1
+ import{execFileSync as P}from"node:child_process";import*as B from"node:fs";import*as q from"node:path";import{request as f}from"node:http";import{WebSocket as L}from"ws";import{probeCDP as I,launchDetachedApp as w}from"../launcher.js";import{withBrowserSessionLock as C}from"../browser-session-lock.js";import{CommandExecutionError as F}from"../errors.js";import{log as U}from"../logger.js";import{addManagedProfile as y,getManagedProfile as _,listManagedProfiles as u,managedProfilesBaseDir as j,removeManagedProfileEntry as g,updateManagedProfile as h}from"./profile.js";import{proxyServerFlag as m}from"./proxy-test.js";import{resolveFingerprint as l,randomSeed as c}from"./fingerprint.js";const d=500,v=20000,A=9301,b=9399;function M(K){for(const Q of K)try{B.accessSync(Q,B.constants.X_OK);return Q}catch{}return null}function i(K){for(const Q of K)try{const Y=P("which",[Q],{encoding:"utf-8",stdio:"pipe"}).trim();if(Y)return Y}catch{}return null}export function discoverChromePath(){const K=process.env.OPENCLI_CHROME_PATH?.trim();if(K)return K;if(process.platform==="darwin")return M(["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]);if(process.platform==="win32"){const Q=process.env.ProgramFiles??"C:\\Program Files",Y=process.env["ProgramFiles(x86)"]??"C:\\Program Files (x86)",Z=process.env.LOCALAPPDATA??"";return M([q.join(Q,"Google","Chrome","Application","chrome.exe"),q.join(Y,"Google","Chrome","Application","chrome.exe"),...Z?[q.join(Z,"Google","Chrome","Application","chrome.exe")]:[],q.join(Q,"Microsoft","Edge","Application","msedge.exe"),q.join(Y,"Microsoft","Edge","Application","msedge.exe")])}return i(["google-chrome-stable","google-chrome","chromium","chromium-browser","microsoft-edge-stable"])}function x(K,Q,Y=5000){return new Promise((Z,H)=>{const X=new URL(Q),$=f({hostname:X.hostname,port:X.port,path:X.pathname+X.search,method:K,timeout:Y},(V)=>{const J=[];V.on("data",(z)=>J.push(Buffer.isBuffer(z)?z:Buffer.from(z)));V.on("end",()=>{const z=Buffer.concat(J).toString("utf8");if((V.statusCode??0)<200||(V.statusCode??0)>=300){H(Error(`HTTP ${V.statusCode}: ${z.slice(0,200)}`));return}try{Z(z?JSON.parse(z):null)}catch{Z(z)}})});$.on("error",H);$.on("timeout",()=>$.destroy(Error(`请求 ${Q} 超时`)));$.end()})}export function managedEndpoint(K){return`http://127.0.0.1:${K.port}`}export async function isManagedChromeRunning(K){return I(K.port)}const p=["--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-domain-reliability","--disable-breakpad","--disable-client-side-phishing-detection","--no-pings","--no-service-autorun","--password-store=basic","--disable-features=Translate,OptimizationHints,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions,CalculateNativeWinOcclusion"];function o(K){const Q=[`--user-data-dir=${K.userDataDir}`,`--remote-debugging-port=${K.port}`,"--remote-allow-origins=*"];if(K.clean!==!1)Q.push(...p);if(K.webrtc!==!1)Q.push("--force-webrtc-ip-handling-policy=disable_non_proxied_udp");if(K.proxy)Q.push(`--proxy-server=${m(K.proxy)}`);if(K.fingerprint?.enabled&&K.fingerprint.locale)Q.push(`--lang=${K.fingerprint.locale}`);Q.push("--no-startup-window");return Q}function s(K){const Q=K.fingerprint?.enabled?K.fingerprint.timezone:void 0;return Q?{TZ:Q}:void 0}async function a(K,Q){const Y=Date.now()+v;while(Date.now()<Y){if(await I(K,1000))return;await new Promise((Z)=>setTimeout(Z,d))}throw new F(`托管 Chrome「${Q}」已启动但 ${v/1000}s 内 CDP 端口 ${K} 未就绪`,"浏览器可能启动缓慢,稍后重试;或检查该端口是否被其它进程占用。")}const t="https://publishport.app/automation";function n(K){if(!K)return!1;try{const Q=new URL(K);return(Q.hostname==="publishport.app"||Q.hostname==="www.publishport.app")&&Q.pathname.endsWith("/automation")}catch{return!1}}async function T(K,Q){const Z=(await x("GET",`${K}/json/version`))?.webSocketDebuggerUrl;if(!Z)throw Error(`CDP 端点 ${K} 没有暴露 browser target`);const H=new L(Z);await new Promise((J,z)=>{const W=setTimeout(()=>{H.close();z(Error("browser CDP 连接超时"))},1e4);H.on("open",()=>{clearTimeout(W);J()});H.on("error",(O)=>{clearTimeout(W);z(O)})});let X=0;const $=new Map;H.on("message",(J)=>{try{const z=JSON.parse(String(J));if(!z.id||!$.has(z.id))return;const W=$.get(z.id);$.delete(z.id);if(z.error)W.reject(Error(z.error.message??"CDP error"));else W.resolve(z.result??{})}catch{}});const V=(J,z={})=>{const W=++X;return new Promise((O,G)=>{const k=setTimeout(()=>{$.delete(W);G(Error(`CDP ${J} 超时`))},1e4);$.set(W,{resolve:(S)=>{clearTimeout(k);O(S)},reject:(S)=>{clearTimeout(k);G(S)}});H.send(JSON.stringify({id:W,method:J,params:z}))})};try{return await Q(V)}finally{H.close()}}async function D(K){const Q=await x("GET",`${K}/json`);return Array.isArray(Q)?Q.filter((Y)=>Y.type==="page"):[]}async function R(K,Q,Y){try{let Z=await D(K);if(Z.length===0){await C(async()=>{if((await D(K)).length>0)return;await T(K,async($)=>{const J=(await $("Target.createTarget",{url:t,background:!0})).targetId;if(J&&Y==="background"){const z=await $("Browser.getWindowForTarget",{targetId:J});if(z.windowId!==void 0)for(let W=0;W<4;W++){await $("Browser.setWindowBounds",{windowId:z.windowId,bounds:{windowState:"minimized"}});await new Promise((G)=>setTimeout(G,250));if((await $("Browser.getWindowBounds",{windowId:z.windowId})).bounds?.windowState==="minimized")break}}})},`anchor-${Q}`);return}const H=Z.filter(($)=>n($.url));if(H.length>1){await T(K,async($)=>{for(const V of H.slice(1))if(V.id)await $("Target.closeTarget",{targetId:V.id})});Z=Z.filter(($)=>!H.slice(1).some((V)=>V.id===$.id))}const X=Z[0];if(Y==="foreground"&&X?.id)await T(K,async($)=>{const V=await $("Browser.getWindowForTarget",{targetId:X.id});if((await $("Browser.getWindowBounds",{windowId:V.windowId})).bounds?.windowState==="minimized")await $("Browser.setWindowBounds",{windowId:V.windowId,bounds:{windowState:"normal"}})})}catch(Z){U.debug(`[managed-chrome] ensurePageTarget 失败(非致命):${Z instanceof Error?Z.message:Z}`)}}const N=new Map;async function E(K){if(await I(K.port))return!0;if(process.platform==="win32")return!1;const Q=q.join(K.userDataDir,"SingletonLock"),Y=()=>{try{B.lstatSync(Q);return!0}catch{return!1}};if(!Y())return!1;for(let Z=0;Z<3;Z++){await new Promise((H)=>setTimeout(H,500));if(await I(K.port,4000))return!0;if(!Y())return!1}return!1}export async function ensureManagedChrome(K,Q,Y={}){const Z=managedEndpoint(Q),H=N.get(K);if(H)return H;if(await E(Q)){U.debug(`[managed-chrome] profile「${K}」已在端口 ${Q.port} 运行`);await R(Z,K,Y.windowMode);return Z}const X=N.get(K);if(X)return X;const $=C(async()=>{if(await E(Q)){U.debug(`[managed-chrome] profile「${K}」等锁期间已被拉起,直接复用`);await R(Z,K,Y.windowMode);return Z}const V=Q.browserPath??discoverChromePath();if(!V)throw new F("找不到可用的 Chrome/Chromium 浏览器。","安装 Google Chrome,或用 OPENCLI_CHROME_PATH 环境变量指定可执行文件路径。");B.mkdirSync(Q.userDataDir,{recursive:!0});U.debug(`[managed-chrome] 拉起 profile「${K}」:${V} 端口 ${Q.port}${Q.proxy?" (代理)":""}`);await w(V,o(Q),`托管 profile ${K}`,s(Q));await a(Q.port,K);await R(Z,K,Y.windowMode);return Z},`launch-${K}`).finally(()=>N.delete(K));N.set(K,$);return $}export async function stopManagedChrome(K){if(!await I(K.port))return!1;const Y=(await x("GET",`${managedEndpoint(K)}/json/version`))?.webSocketDebuggerUrl;if(!Y)throw Error(`端口 ${K.port} 上的 CDP 端点没有暴露 browser target`);await new Promise((H,X)=>{const $=new L(Y),V=setTimeout(()=>{$.close();X(Error("CDP Browser.close 超时"))},1e4);$.on("open",()=>{$.send(JSON.stringify({id:1,method:"Browser.close",params:{}}))});$.on("close",()=>{clearTimeout(V);H()});$.on("error",(J)=>{clearTimeout(V);X(J)})});const Z=Date.now()+5000;while(Date.now()<Z){if(!await I(K.port,500))break;await new Promise((H)=>setTimeout(H,200))}if(process.platform!=="win32"){const H=q.join(K.userDataDir,"SingletonLock"),X=Date.now()+15000;while(Date.now()<X){try{B.lstatSync(H)}catch{return!0}await new Promise(($)=>setTimeout($,200))}U.warn(`[managed-chrome] Chrome 端口已释放但 15s 内 SingletonLock 未消失:${H}`)}return!0}async function r(K){return!await I(K,500)}async function e(){const K=new Set(u().map(({entry:Q})=>Q.port));for(let Q=A;Q<=b;Q++){if(K.has(Q))continue;if(await r(Q))return Q}throw Error(`托管 profile 端口区间 ${A}-${b} 已耗尽`)}export async function resolveManagedOpts(K){const Q={};if(K.label)Q.label=K.label;if(K.proxy)Q.proxy=K.proxy;if(K.webrtc===!1)Q.webrtc=!1;if(K.clean===!1)Q.clean=!1;if(K.fingerprint?.enabled){const Y=K.fingerprint,Z=Y.seed??c();Q.fingerprint=l(Z,{timezone:Y.timezone,locale:Y.locale,userAgent:Y.userAgent,platform:Y.platform,spoof:Y.spoof,hardwareConcurrency:Y.hardwareConcurrency,deviceMemory:Y.deviceMemory,screen:Y.screen,webglVendor:Y.webglVendor,webglRenderer:Y.webglRenderer,canvasNoise:Y.canvasNoise})}return{opts:Q}}export async function createManagedProfile(K,Q={}){const Y=await e(),Z=q.join(j(),K.trim()),H={userDataDir:Z,port:Y,...Q.browserPath?{browserPath:Q.browserPath}:{},...Q.label?{label:Q.label}:{},...Q.proxy?{proxy:Q.proxy}:{},...Q.fingerprint?{fingerprint:Q.fingerprint}:{},...Q.webrtc===!1?{webrtc:!1}:{},...Q.clean===!1?{clean:!1}:{},createdAt:new Date().toISOString()};y(K,H);B.rmSync(Z,{recursive:!0,force:!0});B.mkdirSync(Z,{recursive:!0});return H}export async function updateManagedProfileConfig(K,Q){const Y=_(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);const Z=Q.webrtc!==void 0?Q.webrtc:Y.webrtc,H=Q.clean!==void 0?Q.clean:Y.clean,X={userDataDir:Y.userDataDir,port:Y.port,...Y.browserPath?{browserPath:Y.browserPath}:{},...Y.createdAt?{createdAt:Y.createdAt}:{},...Q.label!==void 0?Q.label?{label:Q.label}:{}:Y.label?{label:Y.label}:{},...Q.proxy!==void 0?Q.proxy?{proxy:Q.proxy}:{}:Y.proxy?{proxy:Y.proxy}:{},...Q.fingerprint!==void 0?Q.fingerprint?{fingerprint:Q.fingerprint}:{}:Y.fingerprint?{fingerprint:Y.fingerprint}:{},...Z===!1?{webrtc:!1}:{},...H===!1?{clean:!1}:{}};let $=!1;try{$=await stopManagedChrome(Y)}catch(V){U.warn(`[managed-chrome] 更新前关闭 profile「${K}」失败(继续):${V instanceof Error?V.message:V}`)}h(K,X);return{entry:X,wasRunning:$}}async function KK(K){const Q=Date.now()+15000;let Y=0;while(Date.now()<Q){try{B.rmSync(K,{recursive:!0,force:!0,maxRetries:3,retryDelay:100})}catch{}await new Promise((Z)=>setTimeout(Z,300));if(!B.existsSync(K)){if(++Y>=2)return}else Y=0}throw new F(`数据目录未能删净(浏览器可能仍在退出中):${K}`,"等浏览器窗口完全关闭后再删一次即可。")}export async function removeManagedProfile(K,Q={}){const Y=_(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);try{await stopManagedChrome(Y)}catch(Z){U.warn(`[managed-chrome] 关闭 profile「${K}」失败(继续删除):${Z instanceof Error?Z.message:Z}`)}if(!Q.keepData){const Z=j(),H=q.resolve(Y.userDataDir);if(H.startsWith(Z+q.sep))await KK(H);else U.warn(`[managed-chrome] user-data-dir 不在 ${Z} 下,跳过数据删除:${H}`)}g(K)}
@@ -1,5 +1,5 @@
1
1
  import*as J from"node:fs";import*as p from"node:os";import*as B from"node:path";import{execSync as e,execFileSync as S}from"node:child_process";import{fileURLToPath as k}from"node:url";import{PLUGINS_DIR as T}from"./discovery.js";import{getErrorMessage as U,PluginError as x}from"./errors.js";import{log as w}from"./logger.js";import{isRecord as b}from"./utils.js";import{readPluginManifest as F,isMonorepo as y,getEnabledPlugins as qq,checkCompatibility as O,getCompatibilityRange as M}from"./plugin-manifest.js";const N=process.platform==="win32",d="local:";function a(){return process.env.HOME||process.env.USERPROFILE||p.homedir()}export function getLockFilePath(){return B.join(a(),".ppcli","plugins.lock.json")}export function getMonoreposDir(){return B.join(a(),".ppcli","monorepos")}function h(q){if(!q)return;if(q.startsWith(d))return{kind:"local",path:B.resolve(q.slice(d.length))};return{kind:"git",url:q}}function isLocalPluginSource(q){return h(q)?.kind==="local"}function toStoredPluginSource(q){if(q.kind==="local")return`${d}${B.resolve(q.path)}`;return q.url}function toLocalPluginSource(q){return toStoredPluginSource({kind:"local",path:q})}function Kq(q){if(!b(q))return;if(typeof q.name!=="string"||typeof q.subPath!=="string")return;return{name:q.name,subPath:q.subPath}}function Qq(q,K){if(typeof q==="string"){const Q=h(q);if(!Q)return;if(Q.kind==="git"&&K)return{kind:"monorepo",url:Q.url,repoName:K.name,subPath:K.subPath};return Q}if(!b(q)||typeof q.kind!=="string")return;switch(q.kind){case"git":return typeof q.url==="string"?{kind:"git",url:q.url}:void 0;case"local":return typeof q.path==="string"?{kind:"local",path:B.resolve(q.path)}:void 0;case"monorepo":return typeof q.url==="string"&&typeof q.repoName==="string"&&typeof q.subPath==="string"?{kind:"monorepo",url:q.url,repoName:q.repoName,subPath:q.subPath}:void 0;default:return}}function Vq(q){if(!b(q))return;const K=Kq(q.monorepo),Q=Qq(q.source,K);if(!Q)return;if(typeof q.commitHash!=="string"||typeof q.installedAt!=="string")return;const V={source:Q,commitHash:q.commitHash,installedAt:q.installedAt};if(typeof q.updatedAt==="string")V.updatedAt=q.updatedAt;return V}function resolvePluginSource(q,K){if(q)return q.source;return h(_q(K))}function resolveStoredPluginSource(q,K){const Q=resolvePluginSource(q,K);return Q?toStoredPluginSource(Q):void 0}function moveDir(q,K,Q=J){try{Q.renameSync(q,K)}catch(V){if(V.code==="EXDEV"){try{Q.cpSync(q,K,{recursive:!0})}catch(Y){try{Q.rmSync(K,{recursive:!0,force:!0})}catch{}throw Y}Q.rmSync(q,{recursive:!0,force:!0})}else throw V}}function L(q,K){const Q=`${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;return B.join(B.dirname(q),`.${B.basename(q)}.${K}-${Q}`)}function Yq(q){const K=B.join(p.tmpdir(),`opencli-clone-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);try{S("git",["clone","--depth","1",q,K],{encoding:"utf-8",stdio:["pipe","pipe","pipe"]})}catch(Q){throw new x(`Failed to clone plugin: ${U(Q)}`,"Check the repository URL and your network connection.")}return K}function m(q,K){const Q=Yq(q);try{return K(Q)}finally{try{J.rmSync(Q,{recursive:!0,force:!0})}catch{}}}function Zq(q,K){const Q=resolvePluginSource(q,K);if(!Q||Q.kind==="local")throw Error(`Unable to determine remote source for plugin at ${K}`);return Q.url}function D(q){try{J.lstatSync(q);return!0}catch{return!1}}function C(q,K){const Q=B.resolve(q,K);if(!Q.startsWith(q+B.sep)&&Q!==q)throw new x(`Plugin path "${K}" escapes repo root.`);return Q}function f(q){try{if(J.lstatSync(q).isSymbolicLink()){J.unlinkSync(q);return}J.rmSync(q,{recursive:!0,force:!0})}catch{}}class i{#K=[];#q=!1;track(q){this.#K.push(q);return q}commit(){if(this.#q)return;this.#q=!0;for(const q of this.#K)q.finalize()}rollback(){if(this.#q)return;this.#q=!0;for(const q of[...this.#K].reverse())q.rollback()}}function s(q){const K=new i;try{const Q=q(K);K.commit();return Q}catch(Q){K.rollback();throw Q}}function n(q,K,Q=J){const V=Q.existsSync(K);Q.mkdirSync(B.dirname(K),{recursive:!0});const Y=L(K,"tmp"),Z=V?L(K,"bak"):null;let X=!1;try{moveDir(q,Y,Q);if(Z)Q.renameSync(K,Z);Q.renameSync(Y,K)}catch($){try{Q.rmSync(Y,{recursive:!0,force:!0})}catch{}if(Z&&!Q.existsSync(K))try{Q.renameSync(Z,K)}catch{}throw $}return{finalize(){if(X)return;X=!0;if(Z)try{Q.rmSync(Z,{recursive:!0,force:!0})}catch{}},rollback(){if(X)return;X=!0;try{Q.rmSync(K,{recursive:!0,force:!0})}catch{}if(Z)try{Q.renameSync(Z,K)}catch{}try{Q.rmSync(Y,{recursive:!0,force:!0})}catch{}}}}function $q(q,K){const Q=D(K);if(Q&&!isSymlinkSync(K))throw Error(`Expected monorepo plugin link at ${K} to be a symlink`);J.mkdirSync(B.dirname(K),{recursive:!0});const V=L(K,"tmp"),Y=Q?L(K,"bak"):null,Z=N?"junction":"dir";let X=!1;try{J.symlinkSync(q,V,Z);if(Y)J.renameSync(K,Y);J.renameSync(V,K)}catch($){f(V);if(Y&&!D(K))try{J.renameSync(Y,K)}catch{}throw $}return{finalize(){if(X)return;X=!0;if(Y)f(Y)},rollback(){if(X)return;X=!0;f(K);if(Y&&!D(K))try{J.renameSync(Y,K)}catch{}f(V)}}}function readLockFileWithWriter(q=writeLockFile){try{const K=J.readFileSync(getLockFilePath(),"utf-8"),Q=JSON.parse(K);if(!b(Q))return{};const V={};let Y=!1;for(const[Z,X]of Object.entries(Q)){const $=Vq(X);if(!$){Y=!0;continue}V[Z]=$;if(JSON.stringify(X)!==JSON.stringify($))Y=!0}if(Y)try{q(V)}catch{}return V}catch{return{}}}export function readLockFile(){return readLockFileWithWriter(writeLockFile)}function writeLockFileWithFs(q,K=J){const Q=getLockFilePath();K.mkdirSync(B.dirname(Q),{recursive:!0});const V=L(Q,"tmp");try{K.writeFileSync(V,JSON.stringify(q,null,2)+`
2
- `);K.renameSync(V,Q)}catch(Y){try{K.rmSync(V,{force:!0})}catch{}throw Y}}export function writeLockFile(q){writeLockFileWithFs(q,J)}export function getCommitHash(q){try{return S("git",["rev-parse","HEAD"],{cwd:q,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return}}export function validatePluginStructure(q){const K=[];if(!J.existsSync(q))return{valid:!1,errors:["Plugin directory does not exist"]};const Q=J.readdirSync(q),V=Q.some((Z)=>Z.endsWith(".ts")&&!Z.endsWith(".d.ts")&&!Z.endsWith(".test.ts")),Y=Q.some((Z)=>Z.endsWith(".js")&&!Z.endsWith(".d.js"));if(!V&&!Y)K.push("No command files found in plugin directory. A plugin must contain at least one .ts or .js command file.");if(V){const Z=B.join(q,"package.json");if(!J.existsSync(Z))K.push('Plugin contains .ts files but no package.json. A package.json with "type": "module" and "@jackwener/opencli" peer dependency is required for TS plugins.');else try{if(JSON.parse(J.readFileSync(Z,"utf-8")).type!=="module")K.push('Plugin package.json must have "type": "module" for TypeScript plugins.')}catch{K.push("Plugin package.json is malformed or invalid JSON.")}}return{valid:K.length===0,errors:K}}function Xq(q){const K=B.join(q,"package.json");if(!J.existsSync(K))return!1;try{const Q=JSON.parse(J.readFileSync(K,"utf-8"));return Q.dependencies!=null&&Object.keys(Q.dependencies).length>0}catch{return!1}}function installDependencies(q){const K=B.join(q,"package.json");if(!J.existsSync(K))return;try{S("npm",["install","--omit=dev"],{cwd:q,encoding:"utf-8",stdio:["pipe","pipe","pipe"],...N&&{shell:!0}})}catch(Q){throw new x(`npm install failed in ${q}: ${U(Q)}`,"Check your network connection and npm configuration.")}}function g(q){xq(q);Tq(q)}function o(q){installDependencies(q);g(q)}function postInstallMonorepoLifecycle(q,K){installDependencies(q);for(const Q of K){if(Q!==q&&Xq(Q))installDependencies(Q);g(Q)}}function l(q){const K=validatePluginStructure(q);if(!K.valid)throw new x(`Invalid plugin structure:
2
+ `);K.renameSync(V,Q)}catch(Y){try{K.rmSync(V,{force:!0})}catch{}throw Y}}export function writeLockFile(q){writeLockFileWithFs(q,J)}export function getCommitHash(q){try{return S("git",["rev-parse","HEAD"],{cwd:q,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return}}export function validatePluginStructure(q){const K=[];if(!J.existsSync(q))return{valid:!1,errors:["Plugin directory does not exist"]};const Q=J.readdirSync(q),V=Q.some((Z)=>Z.endsWith(".ts")&&!Z.endsWith(".d.ts")&&!Z.endsWith(".test.ts")),Y=Q.some((Z)=>Z.endsWith(".js")&&!Z.endsWith(".d.js"));if(!V&&!Y)K.push("No command files found in plugin directory. A plugin must contain at least one .ts or .js command file.");if(V){const Z=B.join(q,"package.json");if(!J.existsSync(Z))K.push('Plugin contains .ts files but no package.json. A package.json with "type": "module" and "@jackwener/opencli" peer dependency is required for TS plugins.');else try{if(JSON.parse(J.readFileSync(Z,"utf-8")).type!=="module")K.push('Plugin package.json must have "type": "module" for TypeScript plugins.')}catch{K.push("Plugin package.json is malformed or invalid JSON.")}}return{valid:K.length===0,errors:K}}function Xq(q){const K=B.join(q,"package.json");if(!J.existsSync(K))return!1;try{const Q=JSON.parse(J.readFileSync(K,"utf-8"));return Q.dependencies!=null&&Object.keys(Q.dependencies).length>0}catch{return!1}}function installDependencies(q){const K=B.join(q,"package.json");if(!J.existsSync(K))return;try{S("npm",["install","--omit=dev","--ignore-scripts"],{cwd:q,encoding:"utf-8",stdio:["pipe","pipe","pipe"],...N&&{shell:!0}})}catch(Q){throw new x(`npm install failed in ${q}: ${U(Q)}`,"Check your network connection and npm configuration.")}}function g(q){xq(q);Tq(q)}function o(q){installDependencies(q);g(q)}function postInstallMonorepoLifecycle(q,K){installDependencies(q);for(const Q of K){if(Q!==q&&Xq(Q))installDependencies(Q);g(Q)}}function l(q){const K=validatePluginStructure(q);if(!K.valid)throw new x(`Invalid plugin structure:
3
3
  - ${K.errors.join(`
4
4
  - `)}`);o(q)}function R(q,K,Q){q[K]={...Q,installedAt:Q.installedAt??new Date().toISOString()}}function r(q,K,Q){s((V)=>{V.track(n(q,K));Q(getCommitHash(K))})}function t(q,K,Q,V,Y){s((Z)=>{if(V){J.mkdirSync(V.parentDir,{recursive:!0});Z.track(n(V.stagingDir,q))}const X=getCommitHash(q);for(const $ of Q){const A=B.join(K,$.name),z=C(q,$.subPath);Z.track($q(z,A))}Y?.(X)})}export function installPlugin(q){const K=parseSource(q);if(!K)throw Error(`Invalid plugin source: "${q}"
5
5
  Supported formats:
@@ -54,4 +54,4 @@ export declare function parseJsonOrThrowLoginWall(response: Response, opts?: {
54
54
  * return res;
55
55
  *
56
56
  * The Node side then calls `throwIfLoginWall(res, { url })` on the result. */
57
- export declare const BROWSER_JSON_SNIFF_FN = "\nasync function fetchJsonOrLoginWall(input, init) {\n const r = await fetch(input, init);\n const contentType = r.headers.get('content-type') || '';\n const text = await r.text();\n const trimmed = text.replace(/^\\s+/, '');\n const looksLikeHtml =\n contentType.toLowerCase().includes('text/html')\n || trimmed.startsWith('<!DOCTYPE')\n || trimmed.startsWith('<!doctype')\n || trimmed.startsWith('<html')\n || trimmed.startsWith('<HTML');\n if (looksLikeHtml) {\n return {\n __loginWall: true,\n status: r.status,\n url: r.url || (typeof input === 'string' ? input : ''),\n contentType,\n bodyPreview: trimmed.slice(0, 100),\n };\n }\n if (!r.ok) {\n return { error: r.status };\n }\n try {\n return JSON.parse(text);\n } catch (err) {\n throw new Error(\n 'JSON parse failed (status=' + r.status + ', body[0..50]=' + JSON.stringify(trimmed.slice(0, 50)) + '): '\n + (err && err.message ? err.message : String(err))\n );\n }\n}\n";
57
+ export declare const BROWSER_JSON_SNIFF_FN = "\nasync function fetchJsonOrLoginWall(input, init) {\n const r = await fetch(input, init);\n const contentType = r.headers.get('content-type') || '';\n const text = await r.text();\n const trimmed = text.replace(/^\\s+/, '');\n const looksLikeHtml =\n contentType.toLowerCase().includes('text/html')\n || /^<(?:!doctype|html|head|body|title)(?:[\\s>/]|$)/i.test(trimmed);\n if (looksLikeHtml) {\n return {\n __loginWall: true,\n status: r.status,\n url: r.url || (typeof input === 'string' ? input : ''),\n contentType,\n bodyPreview: trimmed.slice(0, 100),\n };\n }\n if (!r.ok) {\n return { error: r.status };\n }\n try {\n return JSON.parse(text);\n } catch (err) {\n throw new Error(\n 'JSON parse failed (status=' + r.status + ', body[0..50]=' + JSON.stringify(trimmed.slice(0, 50)) + '): '\n + (err && err.message ? err.message : String(err))\n );\n }\n}\n";
package/dist/src/utils.js CHANGED
@@ -2,7 +2,7 @@ import*as X from"node:fs";import*as Z from"node:path";import C from"turndown";im
2
2
  `});if(q)q(z);return z}export function htmlToMarkdown(q,z){return createMarkdownConverter(z).turndown(q||"").replace(/\u00a0/g," ").replace(/\n{4,}/g,`
3
3
 
4
4
 
5
- `).replace(/[ \t]+$/gm,"").trim()}function H(q){return typeof q==="object"&&q!==null&&q.__loginWall===!0&&typeof q.status==="number"}export function throwIfLoginWall(q,z={}){if(H(q))throw new Y(`Server returned HTML instead of JSON (status=${q.status}). Likely a login wall, rate limit, or WAF challenge.`,q.status,z.url||q.url||"",q.bodyPreview);return q}export async function parseJsonOrThrowLoginWall(q,z={}){const D=q.headers.get("content-type")||"",G=await q.text(),A=G.trimStart();if(D.toLowerCase().includes("text/html")||A.startsWith("<!DOCTYPE")||A.startsWith("<!doctype")||A.startsWith("<html")||A.startsWith("<HTML"))throw new Y(`Server returned HTML instead of JSON (status=${q.status}). Likely a login wall, rate limit, or WAF challenge.`,q.status,z.url||q.url||"",A.slice(0,100));try{return JSON.parse(G)}catch(K){throw Error(`JSON parse failed (status=${q.status}, body[0..50]=${JSON.stringify(A.slice(0,50))}): `+(K instanceof Error?K.message:String(K)))}}export const BROWSER_JSON_SNIFF_FN=`
5
+ `).replace(/[ \t]+$/gm,"").trim()}function H(q){return typeof q==="object"&&q!==null&&q.__loginWall===!0&&typeof q.status==="number"}export function throwIfLoginWall(q,z={}){if(H(q))throw new Y(`Server returned HTML instead of JSON (status=${q.status}). Likely a login wall, rate limit, or WAF challenge.`,q.status,z.url||q.url||"",q.bodyPreview);return q}export async function parseJsonOrThrowLoginWall(q,z={}){const D=q.headers.get("content-type")||"",G=await q.text(),A=G.trimStart();if(D.toLowerCase().includes("text/html")||/^<(?:!doctype|html|head|body|title)(?:[\s>/]|$)/i.test(A))throw new Y(`Server returned HTML instead of JSON (status=${q.status}). Likely a login wall, rate limit, or WAF challenge.`,q.status,z.url||q.url||"",A.slice(0,100));try{return JSON.parse(G)}catch(K){throw Error(`JSON parse failed (status=${q.status}, body[0..50]=${JSON.stringify(A.slice(0,50))}): `+(K instanceof Error?K.message:String(K)))}}export const BROWSER_JSON_SNIFF_FN=`
6
6
  async function fetchJsonOrLoginWall(input, init) {
7
7
  const r = await fetch(input, init);
8
8
  const contentType = r.headers.get('content-type') || '';
@@ -10,10 +10,7 @@ async function fetchJsonOrLoginWall(input, init) {
10
10
  const trimmed = text.replace(/^\\s+/, '');
11
11
  const looksLikeHtml =
12
12
  contentType.toLowerCase().includes('text/html')
13
- || trimmed.startsWith('<!DOCTYPE')
14
- || trimmed.startsWith('<!doctype')
15
- || trimmed.startsWith('<html')
16
- || trimmed.startsWith('<HTML');
13
+ || /^<(?:!doctype|html|head|body|title)(?:[\\s>/]|$)/i.test(trimmed);
17
14
  if (looksLikeHtml) {
18
15
  return {
19
16
  __loginWall: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false