publishport-opencli 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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 w from"node:fs";import*as I from"node:path";import{CommandExecutionError as H,ArgumentError as U}from"@jackwener/opencli/errors";import{cli as Fz,Strategy as Oz}from"@jackwener/opencli/registry";import{assertLiteralContent as Uz}from"../_shared/content-guard.js";import{humanType as x,humanWait as P,humanMoveTo as Bz,elementViewportCenter as vz}from"./human.js";const Dz="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",n=9,l=20,i=3000,o="|||",M="基础",s=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],rz=s.map(([z])=>z),E="文字配图",g="再写一张",r="生成图片",p="下一步",u=".tiptap.ProseMirror",kz=["_onPublish","onPublish","_onSubmit","_handlePublish"],Nz=["_onSave","_onSaveDraft","_onDraft"],m=['[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]"],t=['[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])'],a={".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 Iz(z){return z.map((J)=>{const K=I.resolve(J);if(!w.existsSync(K))throw new U(`Image file not found: ${K}`);const Q=I.extname(K).toLowerCase();if(!a[Q])throw new U(`Unsupported image format "${Q}". Supported: jpg, png, gif, webp`);return K})}const e='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 Pz(z,J=15000){const K=500,Q=Math.max(1,Math.ceil(J/K));for(let Z=0;Z<Q;Z++){if(await z.evaluate(`
2
2
  (() => {
3
- const sels = ${JSON.stringify(s)};
3
+ const sels = ${JSON.stringify(e)};
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(Z<Q-1)await z.wait({time:K/1000})}return!1}async function zz(z,J){if(!await Pz(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,e);return{ok:!0,count:J.length}}catch($){const V=$ instanceof Error?$.message:String($);if(V.includes("Unknown action")||V.includes("not supported")||V.includes("Not allowed"));else return{ok:!1,count:0,error:V}}const Q=J.map(($)=>{const V=w.readFileSync($).toString("base64"),Y=I.extname($).toLowerCase();return{name:I.basename($),mimeType:a[Y],base64:V}}),Z=Q.reduce(($,V)=>$+V.base64.length,0);if(Z>500000)console.warn(`[warn] Total image payload is ${(Z/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(Q);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 Jz(z,J=30000){const K=2000,Q=Math.ceil(J/K);for(let Z=0;Z<Q;Z++){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 Kz(z,J,K,Q){const Z=await z.evaluate(`
51
51
  (function(selectors) {
52
52
  const __opencli_xhs_fill_phase = "locate";
53
53
  for (const sel of selectors) {
@@ -62,7 +62,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
62
62
  }
63
63
  return { ok: false };
64
64
  })(${JSON.stringify(J)})
65
- `);if(!Q.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});throw Error(`Could not find ${Z} input. Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}const q=()=>z.evaluate(`
65
+ `);if(!Z.ok){await z.screenshot({path:`/tmp/xhs_publish_${Q}_debug.png`});throw Error(`Could not find ${Q} input. Debug screenshot: /tmp/xhs_publish_${Q}_debug.png`)}const q=()=>z.evaluate(`
66
66
  ((selector, expectedText) => {
67
67
  const __opencli_xhs_fill_phase = "apply";
68
68
  const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
@@ -118,8 +118,8 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
118
118
  el.blur();
119
119
  const actual = normalize(el.innerText || el.textContent || '');
120
120
  return { ok: actual === normalize(expectedText), actual };
121
- })(${JSON.stringify(Q.sel)}, ${JSON.stringify(K)})
122
- `);let $;if(Q.kind==="contenteditable"&&z.insertText){if(!(await z.evaluate(`
121
+ })(${JSON.stringify(Z.sel)}, ${JSON.stringify(K)})
122
+ `);let $;if(Z.kind==="contenteditable"&&z.insertText){if(!(await z.evaluate(`
123
123
  ((selector, nextText) => {
124
124
  const __opencli_xhs_fill_phase = "prepare";
125
125
  const fireBeforeInput = (el, value) => {
@@ -145,8 +145,8 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
145
145
  selection?.addRange(range);
146
146
  fireBeforeInput(el, nextText);
147
147
  return { ok: true };
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(`
148
+ })(${JSON.stringify(Z.sel)}, ${JSON.stringify(K)})
149
+ `))?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Q}_debug.png`});throw Error(`Could not prepare ${Q} input. Debug screenshot: /tmp/xhs_publish_${Q}_debug.png`)}try{await x(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();
@@ -168,8 +168,8 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
168
168
  el.blur();
169
169
  const actual = normalize(el.innerText || el.textContent || '');
170
170
  return { ok: actual === normalize(expectedText), actual };
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(`
171
+ })(${JSON.stringify(Z.sel)}, ${JSON.stringify(K)})
172
+ `)}catch{$=await q()}}else $=await q();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Q}_debug.png`});const V=typeof $?.actual==="string"?$.actual:"";throw Error(`Failed to set ${Q}. Expected "${K}", got "${V}". Debug screenshot: /tmp/xhs_publish_${Q}_debug.png`)}}async function Mz(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 pz(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 tz(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 Qz(z,J){return`
260
260
  ((topicName, selectors) => {
261
261
  const __opencli_xhs_topic_marker_count = true;
262
262
  const marker = '#' + topicName + '[话题]';
@@ -274,14 +274,24 @@ 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 az(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
+ `))}function Zz(z){return`
285
+ (selectors => {
286
+ let editor = null;
287
+ for (const sel of selectors) {
288
+ editor = document.querySelector(sel);
289
+ if (editor) break;
290
+ }
291
+ if (!editor) return -1;
292
+ return (editor.innerText || editor.textContent || '').length;
293
+ })(${JSON.stringify(z)})
294
+ `}async function Lz(z,J,K){if(!await Mz(z,J))throw new H(`Could not attach topic "${K}": body editor not found`);const Z=Number(W(await z.evaluate(Qz(K,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 "${K}": page.insertText is unavailable`);try{await x(z,`#${K}`)}catch{throw new H(`Could not attach topic "${K}": failed to type inline topic query`)}await P(z,1.2);if(typeof z.pressKey!=="function")throw new H(`Could not attach topic "${K}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch($){throw new H(`Could not attach topic "${K}": failed to accept suggestion (${$&&$.message||$})`)}await z.wait({time:0.8});if((Number(W(await z.evaluate(Qz(K,J))))||0)<=Z)throw new H(`Could not attach topic "${K}": no real topic entity appeared after selection`)}async function _z(z,J,K){const Q=[],Z=[];for(const q of K){let $=-1;try{const V=Number(W(await z.evaluate(Zz(J))));$=Number.isFinite(V)?V:-1}catch{}try{await Lz(z,J,q);Q.push(q);await z.wait({time:0.4})}catch(V){Z.push({topic:q,reason:V&&V.message||String(V)});if($>=0&&typeof z.pressKey==="function")try{const Y=q.length+6;for(let G=0;G<Y;G++){const O=Number(W(await z.evaluate(Zz(J))));if(!Number.isFinite(O)||O<0||O<=$)break;await z.pressKey("Backspace");await z.wait({time:0.15})}}catch{}}}return{added:Q,failed:Z}}async function hz(z){const J=await z.evaluate(`
285
295
  () => {
286
296
  const isVisible = (el) => {
287
297
  if (!el || el.offsetParent === null) return false;
@@ -330,7 +340,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
330
340
  }
331
341
  return { ok: false, visibleTexts };
332
342
  }
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(`
343
+ `);if(J?.ok)await z.wait({time:1});return J}async function L(z,J,K=7000){const Q=500,Z=Math.max(1,Math.ceil(K/Q));let q={ok:!1};for(let $=0;$<Z;$++){q=W(await z.evaluate(`
334
344
  ((cfg) => {
335
345
  const __opencli_xhs_click_label = { wantLabel: ${JSON.stringify(J)} };
336
346
  const wantLabel = ${JSON.stringify(J)};
@@ -365,10 +375,10 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
365
375
  }
366
376
  return { ok: false };
367
377
  })()
368
- `));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function Dz(z){const J=await z.evaluate(`
378
+ `));if(q?.ok)return q;await z.wait({time:Q/1000})}return q}async function fz(z){const J=await z.evaluate(`
369
379
  (() => {
370
380
  const __opencli_xhs_focus_card = true;
371
- const sel = ${JSON.stringify(_)};
381
+ const sel = ${JSON.stringify(u)};
372
382
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
373
383
  // Prefer the editor inside the active swiper slide if present.
374
384
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
@@ -382,34 +392,34 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
382
392
  selection?.addRange(range);
383
393
  return { ok: true };
384
394
  })()
385
- `);return W(J)}async function fz(z){const J=await z.evaluate(`
395
+ `);return W(J)}async function Az(z){const J=await z.evaluate(`
386
396
  (() => {
387
397
  const __opencli_xhs_card_text = true;
388
- const sel = ${JSON.stringify(_)};
398
+ const sel = ${JSON.stringify(u)};
389
399
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
390
400
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
391
401
  const text = active ? (active.innerText || active.textContent || '').trim() : '';
392
402
  return { ok: !!text, text };
393
403
  })()
394
- `);return W(J)}async function a(z){const J=await z.evaluate(`
404
+ `);return W(J)}async function $z(z){const J=await z.evaluate(`
395
405
  (() => {
396
406
  const __opencli_xhs_card_count = true;
397
- const sel = ${JSON.stringify(_)};
407
+ const sel = ${JSON.stringify(u)};
398
408
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
399
409
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
400
410
  const activeText = active ? (active.innerText || active.textContent || '').trim() : '';
401
411
  return { ok: true, count: editors.length, activeEmpty: !activeText };
402
412
  })()
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(`
413
+ `);return W(J)}async function Rz(z,J=8000){const K=300,Q=Math.ceil(J/K);for(let Z=0;Z<Q;Z++){if((await $z(z))?.count>=1)return!0;await z.wait({time:K/1000})}return!1}async function wz(z,J,K=6000){const Q=300,Z=Math.ceil(K/Q);for(let q=0;q<Z;q++){const $=await $z(z);if($?.count>=J&&$?.activeEmpty)return!0;await z.wait({time:Q/1000})}return!1}async function yz(z,J,K=4){for(let Q=0;Q<K;Q++){if(!(await L(z,g))?.ok)return!1;if(await wz(z,J,2500))return!0}return!1}async function bz(z){const J=await z.evaluate(`
404
414
  (() => {
405
415
  const __opencli_xhs_preview_ready = true;
406
416
  const ready = Array.from(document.querySelectorAll('button'))
407
417
  .some((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '下一步');
408
418
  return { ok: ready };
409
419
  })()
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(`
420
+ `);return W(J)}async function Cz(z,J=6){const K=400,Q=Math.ceil(3000/K);for(let Z=0;Z<J;Z++){if(!(await L(z,r))?.ok&&Z===0)return!1;for(let $=0;$<Q;$++){if((await bz(z))?.ok)return!0;await z.wait({time:K/1000})}}return!1}async function Tz(z,J,K){J=String(J).replace(/\\n/g,`
421
+ `);if(!(await fz(z))?.ok)throw new H(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const q=J.split(`
422
+ `);for(let $=0;$<q.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(q[$])await x(z,q[$])}}else await z.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(J)})`);await z.wait({time:0.4});if(!(await Az(z))?.ok)throw new H(`文字配图: card editor #${K+1} is empty after typing`)}async function Sz(z,J){if(!J||J===M)return M;const K=W(await z.evaluate(`
413
423
  (async () => {
414
424
  const __opencli_xhs_card_styles = true;
415
425
  const want = ${JSON.stringify(J)};
@@ -438,7 +448,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
438
448
  }
439
449
  return { ok: seen.length > 0, styles: seen, found: false };
440
450
  })()
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(`
451
+ `));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 ${M}.`);if(!(await L(z,J))?.ok)throw new H(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function qz(z){const J=await z.evaluate(`
442
452
  (() => {
443
453
  const __opencli_xhs_composer_media_count = true;
444
454
  const visibleBox = (el) => {
@@ -451,7 +461,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
451
461
  const r = el.getBoundingClientRect();
452
462
  return r.width >= 48 && r.height >= 48;
453
463
  };
454
- const titleSelectors = ${JSON.stringify(A)};
464
+ const titleSelectors = ${JSON.stringify(m)};
455
465
  const titleEl = titleSelectors
456
466
  .map((sel) => Array.from(document.querySelectorAll(sel)))
457
467
  .flat()
@@ -470,7 +480,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
470
480
  }
471
481
  return { ok: true, count };
472
482
  })()
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(`
483
+ `);return W(J)}async function Vz(z,J,K){const Q=await qz(z);if(!Q||typeof Q.count!=="number")throw new H(`${K}: could not verify current composer media count`);if(Q.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 ${Q.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function xz(z,J,K){if(!(await L(z,E))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: could not click "${E}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await Rz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: 写文字 card editor did not appear after clicking "${E}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let V=0;V<J.length;V++){if(V>0){if(!await yz(z,V+1)){await z.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new H(`文字配图: new card editor #${V+1} did not render after "${g}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await Tz(z,J[V],V)}if(!await Cz(z)){await z.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new H(`文字配图: "${r}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const q=await Sz(z,K);if(!(await L(z,p))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new H(`文字配图: could not click "${p}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return q}async function jz(z){return z.evaluate(`
474
484
  () => {
475
485
  const text = (document.body?.innerText || '').replace(/s+/g, ' ').trim();
476
486
  const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
@@ -500,22 +510,22 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
500
510
  const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
501
511
  return { state, hasTitleInput, hasImageInput, hasVideoSurface };
502
512
  }
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(`
513
+ `)}async function Ez(z,J=5000){const K=500,Q=Math.max(1,Math.ceil(J/K));let Z=await jz(z);for(let q=0;q<Q;q++){if(Z.state!=="video_surface")return Z;if(q<Q-1){await z.wait({time:K/1000});Z=await jz(z)}}return Z}async function uz(z,J=1e4){const K=1000,Q=Math.ceil(J/K);for(let Z=0;Z<Q;Z++){if(await z.evaluate(`
504
514
  (() => {
505
- const sels = ${JSON.stringify(A)};
515
+ const sels = ${JSON.stringify(m)};
506
516
  for (const sel of sels) {
507
517
  const el = document.querySelector(sel);
508
518
  if (el && el.offsetParent !== null) return true;
509
519
  }
510
520
  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(`
521
+ })()`))return!0;if(Z<Q-1)await z.wait({time:K/1000})}return!1}async function mz(z){const J=W(await z.evaluate(`
512
522
  (() => {
513
523
  const __opencli_xhs_restore_srcs = true;
514
524
  return Array.from(document.querySelectorAll('.img-container img'))
515
525
  .map((img) => img.currentSrc || img.src || '')
516
526
  .filter(Boolean);
517
527
  })()
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(`
528
+ `));return Array.isArray(J)?J:[]}async function Hz(z,J){if(!Array.isArray(J)||J.length===0)return 0;const K=W(await z.evaluate(`
519
529
  (async (wanted) => {
520
530
  const __opencli_xhs_delete_by_src = true;
521
531
  const want = new Set(wanted);
@@ -546,7 +556,16 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
546
556
  }
547
557
  return removed;
548
558
  })(${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(`
559
+ `));return Number(K)||0}function cz(z){const J=String(z&&(z.url||z.key)||""),K=String(z&&z.method||"").toUpperCase(),Q=Number(z&&z.status||0);if(K!=="PUT")return null;if(!/ros-upload[^/]*\/spectrum\//.test(J))return null;if(Q<200||Q>=300)return null;const Z=J.match(/\/spectrum\/([^?#]+)/);return Z?Z[1]:J}function Gz(z){return Math.min(45,6+z*4)}async function Yz(z,J,K,Q=90000){if(J<=0)return{ok:!0,completed:0};if(!K||typeof z.readNetworkCapture!=="function"){await z.wait({time:Gz(J)});return{ok:!0,completed:null,viaFallback:!0}}const Z=1500,$=Math.ceil(9000/Z),V=Math.ceil(Q/Z),Y=new Set;for(let G=0;G<V;G++){const O=await z.readNetworkCapture().catch(()=>[]);for(const v of Array.isArray(O)?O:[]){const X=cz(v);if(X)Y.add(X)}if(Y.size>=J)return{ok:!0,completed:Y.size};if(G>=$&&Y.size===0){await z.wait({time:Gz(J)});return{ok:!0,completed:null,viaFallback:!0}}await z.wait({time:Z/1000})}return{ok:!1,completed:Y.size}}async function dz(z,J,K){try{await z.goto("https://creator.xiaohongshu.com/new/note-manager")}catch{return null}const Q=J.trim();for(let Z=0;Z<K;Z++){await z.wait({time:1.5});let q;try{q=W(await z.evaluate(`(() => {
560
+ const noteIdRe = /"noteId":"([0-9a-f]{24})"/;
561
+ return Array.from(document.querySelectorAll('div.note[data-impression], div.note')).slice(0, 3).map((card) => {
562
+ const impression = card.getAttribute('data-impression') || '';
563
+ const id = impression.match(noteIdRe)?.[1] || '';
564
+ const title = (card.querySelector('.title, .raw')?.innerText || '').trim();
565
+ const dateText = (card.querySelector('.time_status, .time')?.innerText || '').trim();
566
+ return { id, title, dateText };
567
+ }).filter((entry) => entry.title);
568
+ })()`))}catch{return null}if(Array.isArray(q)){const $=q.find((V)=>{const Y=String(V.title||"").trim();if(!(Y===Q||Y.startsWith(Q)||Q.startsWith(Y)))return!1;const G=String(V.dateText||"").match(/(\d{4})年(\d{2})月(\d{2})日 (\d{2}):(\d{2})/);if(!G)return!0;const O=Date.UTC(Number(G[1]),Number(G[2])-1,Number(G[3]),Number(G[4]),Number(G[5]))-28800000;return Math.abs(Date.now()-O)<=93600000});if($)return $}}return null}Fz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Oz.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:`文字配图卡片文字,多张卡片用 ${o} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${M}。可选: ${s.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号。平台话题库匹配不上的会自动跳过并在结果中提示,不影响发布"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail","url"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Q=J.file?String(J.file).trim():"";let Z=String(J.content??"").trim();if(Q){if(Z)throw new U("正文位置参数和 --file 只能二选一,不能同时给");const j=I.resolve(Q);if(!w.existsSync(j))throw new U(`--file 指向的文件不存在: ${j}`);Z=w.readFileSync(j,"utf-8").trim();if(!Z)throw new U(`--file 指向的文件内容为空: ${j}`)}const q=J.images?String(J.images).split(",").map((j)=>j.trim()).filter(Boolean):[],$=J.topics?String(J.topics).split(",").map((j)=>j.trim()).filter(Boolean):[],V=Boolean(J.draft),Y=J["card-text"]?String(J["card-text"]):"",G=Y?Y.split(o).map((j)=>j.trim()).filter(Boolean):[],O=J["card-style"]?String(J["card-style"]).trim():"",v=G.length>0;if(!K)throw new U("--title is required");if(K.length>l)throw new U(`Title is ${K.length} chars — must be ≤ ${l}`);if(!Z)throw new U("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");Uz(Z,{fileFlag:"--file"});if(!v&&q.length===0)throw new U("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>n)throw new U(`Too many images: ${q.length} (max ${n})`);if(v&&q.some((j)=>I.extname(j).toLowerCase()===".gif"))throw new U("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const X=Iz(q);await z.goto(Dz);await z.wait({time:1});await z.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let _="";for(let j=0;j<30;j++){await z.wait({time:0.5});_=await z.evaluate("() => location.href");if(_.includes("creator.xiaohongshu.com"))break}if(!_.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${_}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const h=await hz(z);if((await Ez(z,h?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const j=h?.ok?`clicked "${h.text}"`:`visible candidates: ${(h?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${j}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const f=await mz(z);let A=O;if(v)A=await xz(z,G,O);else{const j=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,B=await zz(z,X);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`图片注入失败:${B.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await z.wait({time:i/1000});const D=await Yz(z,X.length,j);if(!D.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`图片上传未全部完成:只确认 ${D.completed}/${X.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await Jz(z);if(f.length){await Hz(z,f);await z.wait({time:0.5})}const R=await qz(z);if(R&&typeof R.count==="number"&&R.count>X.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new H(`清掉恢复态旧图后编辑器内仍有 ${R.count} 张图,超过本次注入的 ${X.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await uz(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(v)await Vz(z,G.length,"文字配图 generated images");if(v&&X.length>0){const j=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,B=await zz(z,X);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`追加图片失败:${B.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await z.wait({time:i/1000});const D=await Yz(z,X.length,j);if(!D.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`追加图片上传未全部完成:只确认 ${D.completed}/${X.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await Jz(z);await Vz(z,G.length+X.length,"文字配图 appended images")}if(v&&f.length){await Hz(z,f);await z.wait({time:0.5})}await Kz(z,m,K,"title");await P(z,0.6);await Kz(z,t,Z,"content");await P(z,0.6);let y=[],b=[];if($.length){const j=await _z(z,t,$);y=j.added;b=j.failed}await P(z,1,0.6);const C=await vz(z,["xhs-publish-btn"]);if(C){await Bz(z,C.x,C.y);await P(z,0.4)}const c=V?["暂存离开","存草稿"]:["发布","发布笔记"],F=await z.evaluate(`
550
569
  (cfg => {
551
570
  const { isDraftMode, publishNames, draftNames, labels } = cfg;
552
571
  const isVisible = (el) => {
@@ -585,8 +604,8 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
585
604
  }
586
605
  }
587
606
  return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
588
- })(${JSON.stringify({isDraftMode:j,publishNames:Xz,draftNames:Wz,labels:T})})
589
- `);if(!X?.ok){if(j){if(await z.evaluate(`
607
+ })(${JSON.stringify({isDraftMode:V,publishNames:kz,draftNames:Nz,labels:c})})
608
+ `);if(!F?.ok){if(V){if(await z.evaluate(`
590
609
  (() => {
591
610
  const labels = ['返回', '关闭', '取消', '离开'];
592
611
  const buttons = document.querySelectorAll('button, [role="button"], div, span');
@@ -599,7 +618,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
599
618
  }
600
619
  return false;
601
620
  })()
602
- `)){await z.wait({time:1});const O=["暂存离开","存草稿","保存草稿"];if(await z.evaluate(`
621
+ `)){await z.wait({time:1});const B=["暂存离开","存草稿","保存草稿"];if(await z.evaluate(`
603
622
  (labels => {
604
623
  const buttons = document.querySelectorAll('button, [role="button"]');
605
624
  for (const btn of buttons) {
@@ -614,8 +633,8 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
614
633
  }
615
634
  }
616
635
  return false;
617
- })(${JSON.stringify(O)})
618
- `)){X.ok=!0;X.via="leave-save-fallback"}}if(!X?.ok){await z.wait({time:2});if(await z.evaluate(`
636
+ })(${JSON.stringify(B)})
637
+ `)){F.ok=!0;F.via="leave-save-fallback"}}if(!F?.ok){await z.wait({time:2});if(await z.evaluate(`
619
638
  () => {
620
639
  const markers = ['草稿箱(', '保存于', '编辑于'];
621
640
  for (const el of document.querySelectorAll('*')) {
@@ -624,7 +643,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
624
643
  }
625
644
  return false;
626
645
  }
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(`
646
+ `)){F.ok=!0;F.via="auto-save"}}}}if(!F?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const j=F?.via?` (via=${F.via})`:"",B=F?.error?`, error=${F.error}`:"",D=F?.lastMethodError?`, lastMethodError=${F.lastMethodError}`:"";throw Error(`未能触发「${c[0]}」按钮${j}${B}${D}。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_submit_debug.png")}await z.wait({time:4});const N=await z.evaluate("() => location.href"),Xz=V?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],d=await z.evaluate(`
628
647
  (markers => {
629
648
  for (const el of document.querySelectorAll('*')) {
630
649
  if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
@@ -633,5 +652,5 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
633
652
  if (el.children.length === 0 && markers.some(marker => text.includes(marker))) return text;
634
653
  }
635
654
  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(" · ")}]}});
655
+ })(${JSON.stringify(Xz)})
656
+ `),Wz=!N.includes("/publish/publish");let T=d.length>0||Wz;const S=V?"暂存成功":"发布成功";let k=null;if(!V){k=await dz(z,K,T?4:8);if(k)T=!0}if(!T){if(V)throw new H(`${S}无法确认:未观察到成功提示或提交后跳转。`+(N?`当前 URL: ${N}。`:"当前 URL 为空。")+"先用 `xiaohongshu drafts` 查草稿箱确认是否已存上;确认没有再重新运行本命令,避免堆出重复草稿。");throw new H(`${S}无法确认:未观察到成功提示或提交后跳转,创作者后台也暂未查到同标题笔记。`+(N?`当前 URL: ${N}。`:"")+"注意:发布按钮已被触发,笔记仍可能延迟入库。禁止立即重发——请等 1-2 分钟后用 `xiaohongshu creator-notes` 回查本篇标题是否已出现;确认不存在后才重新运行本 publish 命令,否则会重复发布。")}return[{status:`✅ ${S}`,detail:[`"${K}"`,v?`${G.length}张文字配图${X.length?` + ${X.length}张图片`:""}${A&&A!==M?` (${A})`:""}`:`${X.length}张图片`,y.length?`话题: ${y.join(" ")}`:"",b.length?`⚠️ 话题未附上(已自动跳过,不影响发布): ${b.map((j)=>j.topic).join(" ")}`:"",k?.id?`笔记ID: ${k.id}`:"",d||(k?"已在创作者后台确认":"")||N||""].filter(Boolean).join(" · "),url:k?.id?`https://www.xiaohongshu.com/explore/${k.id}`:""}]}});
@@ -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.8",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false