publishport-opencli 1.0.9 → 1.0.10

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.
Files changed (42) hide show
  1. package/clis/_shared/file-inject.js +41 -0
  2. package/clis/_shared/video-publish.js +4 -4
  3. package/clis/bilibili/upload.js +1 -1
  4. package/clis/chatgpt/utils.js +5 -5
  5. package/clis/claude/utils.js +1 -1
  6. package/clis/deepseek/utils.js +7 -7
  7. package/clis/douyin/draft.js +6 -6
  8. package/clis/douyin/publish-image.js +10 -31
  9. package/clis/facebook/post.js +9 -36
  10. package/clis/instagram/post.js +19 -66
  11. package/clis/instagram/reel.js +11 -11
  12. package/clis/threads/post.js +15 -34
  13. package/clis/twitter/post.js +15 -42
  14. package/clis/twitter/utils.js +4 -36
  15. package/clis/wechat-channels/publish.js +29 -57
  16. package/clis/weibo/publish.js +8 -8
  17. package/clis/weixin/create-draft.js +7 -7
  18. package/clis/xianyu/publish.js +2 -2
  19. package/clis/xiaohongshu/publish-video.js +9 -27
  20. package/clis/xiaohongshu/publish.js +32 -72
  21. package/dist/src/browser/bridge-readiness.d.ts +1 -1
  22. package/dist/src/browser/daemon-client.d.ts +72 -1
  23. package/dist/src/browser/daemon-client.js +1 -1
  24. package/dist/src/browser/daemon-version.d.ts +1 -1
  25. package/dist/src/browser/errors.d.ts +2 -0
  26. package/dist/src/browser/errors.js +3 -3
  27. package/dist/src/browser/network-interceptor.d.ts +11 -0
  28. package/dist/src/browser/network-interceptor.js +1 -0
  29. package/dist/src/cli.js +24 -24
  30. package/dist/src/commands/daemon.js +1 -1
  31. package/dist/src/daemon-utils.d.ts +1 -0
  32. package/dist/src/daemon-utils.js +1 -1
  33. package/dist/src/daemon.js +1 -1
  34. package/dist/src/doctor.d.ts +1 -1
  35. package/dist/src/doctor.js +7 -7
  36. package/dist/src/errors.d.ts +8 -0
  37. package/dist/src/errors.js +1 -1
  38. package/dist/src/execution.js +2 -2
  39. package/dist/src/session-lease.d.ts +135 -0
  40. package/dist/src/session-lease.js +1 -0
  41. package/dist/src/session-lease.test.d.ts +1 -0
  42. package/package.json +1 -1
@@ -1,53 +1,13 @@
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(`
1
+ import*as S from"node:fs";import*as R from"node:path";import{CommandExecutionError as j,ArgumentError as U}from"@jackwener/opencli/errors";import{cli as Wz,Strategy as Fz}from"@jackwener/opencli/registry";import{assertLiteralContent as Oz}from"../_shared/content-guard.js";import{setInputFiles as Uz}from"../_shared/file-inject.js";import{humanType as x,humanWait as N,humanMoveTo as Bz,elementViewportCenter as Dz}from"./human.js";const vz="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",n=9,l=20,i=3000,o="|||",I="基础",s=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],tz=s.map(([z])=>z),E="文字配图",g="再写一张",r="生成图片",p="下一步",u=".tiptap.ProseMirror",Pz=["_onPublish","onPublish","_onSubmit","_handlePublish"],kz=["_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])'],Nz={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp"};function Y(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=R.resolve(J);if(!S.existsSync(K))throw new U(`Image file not found: ${K}`);const Q=R.extname(K).toLowerCase();if(!Nz[Q])throw new U(`Unsupported image format "${Q}". Supported: jpg, png, gif, webp`);return K})}const a='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 hz(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(e)};
3
+ const sels = ${JSON.stringify(a)};
4
4
  return !!document.querySelector(sels);
5
5
  })()
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
- (async () => {
8
- const images = ${q};
9
-
10
- const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
11
- const input = inputs.find(el => {
12
- const accept = el.getAttribute('accept') || '';
13
- return (
14
- accept.includes('image') ||
15
- accept.includes('.jpg') ||
16
- accept.includes('.jpeg') ||
17
- accept.includes('.png') ||
18
- accept.includes('.gif') ||
19
- accept.includes('.webp')
20
- );
21
- });
22
-
23
- if (!input) return { ok: false, count: 0, error: 'No image file input found on page' };
24
-
25
- const dt = new DataTransfer();
26
- for (const img of images) {
27
- try {
28
- const binary = atob(img.base64);
29
- const bytes = new Uint8Array(binary.length);
30
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
31
- const blob = new Blob([bytes], { type: img.mimeType });
32
- dt.items.add(new File([blob], img.name, { type: img.mimeType }));
33
- } catch (e) {
34
- return { ok: false, count: 0, error: 'Failed to create File: ' + e.message };
35
- }
36
- }
37
-
38
- // configurable:true 让同一个 input 能被反复注入(重试 / 追加时 SPA 常复用同一元素,
39
- // 否则第二次 defineProperty 会抛 "Cannot redefine property: files")。
40
- Object.defineProperty(input, 'files', { value: dt.files, writable: false, configurable: true });
41
- input.dispatchEvent(new Event('change', { bubbles: true }));
42
- input.dispatchEvent(new Event('input', { bubbles: true }));
43
-
44
- return { ok: true, count: dt.files.length };
45
- })()
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(`
6
+ `))return!0;if(Z<Q-1)await z.wait({time:K/1000})}return!1}async function e(z,J){if(!await hz(z))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};try{return{ok:!0,count:(await Uz(z,a,J)).count??J.length}}catch(Q){return{ok:!1,count:0,error:Q instanceof Error?Q.message:String(Q)}}}async function zz(z,J=30000){const K=2000,Q=Math.ceil(J/K);for(let Z=0;Z<Q;Z++){if(!await z.evaluate(`
47
7
  () => !!document.querySelector(
48
8
  '[class*="upload"][class*="progress"], [class*="uploading"], [class*="loading"][class*="image"]'
49
9
  )
50
- `))return;await z.wait({time:K/1000})}}async function Kz(z,J,K,Q){const Z=await z.evaluate(`
10
+ `))return;await z.wait({time:K/1000})}}async function Jz(z,J,K,Q){const Z=await z.evaluate(`
51
11
  (function(selectors) {
52
12
  const __opencli_xhs_fill_phase = "locate";
53
13
  for (const sel of selectors) {
@@ -62,7 +22,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
62
22
  }
63
23
  return { ok: false };
64
24
  })(${JSON.stringify(J)})
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(`
25
+ `);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 $=()=>z.evaluate(`
66
26
  ((selector, expectedText) => {
67
27
  const __opencli_xhs_fill_phase = "apply";
68
28
  const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
@@ -119,7 +79,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
119
79
  const actual = normalize(el.innerText || el.textContent || '');
120
80
  return { ok: actual === normalize(expectedText), actual };
121
81
  })(${JSON.stringify(Z.sel)}, ${JSON.stringify(K)})
122
- `);let $;if(Z.kind==="contenteditable"&&z.insertText){if(!(await z.evaluate(`
82
+ `);let q;if(Z.kind==="contenteditable"&&z.insertText){if(!(await z.evaluate(`
123
83
  ((selector, nextText) => {
124
84
  const __opencli_xhs_fill_phase = "prepare";
125
85
  const fireBeforeInput = (el, value) => {
@@ -146,7 +106,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
146
106
  fireBeforeInput(el, nextText);
147
107
  return { ok: true };
148
108
  })(${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(`
109
+ `))?.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);q=await z.evaluate(`
150
110
  ((selector, expectedText) => {
151
111
  const __opencli_xhs_fill_phase = "verify";
152
112
  const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
@@ -169,7 +129,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
169
129
  const actual = normalize(el.innerText || el.textContent || '');
170
130
  return { ok: actual === normalize(expectedText), actual };
171
131
  })(${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(`
132
+ `)}catch{q=await $()}}else q=await $();if(!q?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Q}_debug.png`});const V=typeof q?.actual==="string"?q.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 Y(await z.evaluate(`
173
133
  (selectors => {
174
134
  const el = selectors
175
135
  .map(sel => Array.from(document.querySelectorAll(sel)))
@@ -185,7 +145,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
185
145
  selection?.addRange(range);
186
146
  return true;
187
147
  })(${JSON.stringify(J)})
188
- `))}function pz(z,{click:J=!1}={}){return`
148
+ `))}function az(z,{click:J=!1}={}){return`
189
149
  (topicName => {
190
150
  const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
191
151
  const want = norm(topicName);
@@ -225,7 +185,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
225
185
  text: (target.innerText || target.textContent || '').trim().slice(0, 40),
226
186
  };
227
187
  })(${JSON.stringify(z)})
228
- `}function tz(z,J){return`
188
+ `}function ez(z,J){return`
229
189
  ((topicName, selectors) => {
230
190
  const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
231
191
  const want = norm(topicName);
@@ -256,7 +216,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
256
216
  }
257
217
  return count;
258
218
  })(${JSON.stringify(z)}, ${JSON.stringify(J)})
259
- `}function Qz(z,J){return`
219
+ `}function Kz(z,J){return`
260
220
  ((topicName, selectors) => {
261
221
  const __opencli_xhs_topic_marker_count = true;
262
222
  const marker = '#' + topicName + '[话题]';
@@ -274,14 +234,14 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
274
234
  }
275
235
  return count;
276
236
  })(${JSON.stringify(z)}, ${JSON.stringify(J)})
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(`
237
+ `}async function zJ(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return Y(await z.evaluate(`
278
238
  (text => {
279
239
  const ok = document.execCommand('insertText', false, text);
280
240
  const active = document.activeElement;
281
241
  if (active) active.dispatchEvent(new Event('input', { bubbles: true }));
282
242
  return ok;
283
243
  })(${JSON.stringify(K)})
284
- `))}function Zz(z){return`
244
+ `))}function Qz(z){return`
285
245
  (selectors => {
286
246
  let editor = null;
287
247
  for (const sel of selectors) {
@@ -291,7 +251,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
291
251
  if (!editor) return -1;
292
252
  return (editor.innerText || editor.textContent || '').length;
293
253
  })(${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(`
254
+ `}async function Lz(z,J,K){if(!await Mz(z,J))throw new j(`Could not attach topic "${K}": body editor not found`);const Z=Number(Y(await z.evaluate(Kz(K,J))))||0;if(typeof z.pressKey==="function")try{await z.pressKey("Enter")}catch{}if(typeof z.insertText!=="function")throw new j(`Could not attach topic "${K}": page.insertText is unavailable`);try{await x(z,`#${K}`)}catch{throw new j(`Could not attach topic "${K}": failed to type inline topic query`)}await N(z,1.2);if(typeof z.pressKey!=="function")throw new j(`Could not attach topic "${K}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch(q){throw new j(`Could not attach topic "${K}": failed to accept suggestion (${q&&q.message||q})`)}await z.wait({time:0.8});if((Number(Y(await z.evaluate(Kz(K,J))))||0)<=Z)throw new j(`Could not attach topic "${K}": no real topic entity appeared after selection`)}async function _z(z,J,K){const Q=[],Z=[];for(const $ of K){let q=-1;try{const V=Number(Y(await z.evaluate(Qz(J))));q=Number.isFinite(V)?V:-1}catch{}try{await Lz(z,J,$);Q.push($);await z.wait({time:0.4})}catch(V){Z.push({topic:$,reason:V&&V.message||String(V)});if(q>=0&&typeof z.pressKey==="function")try{const W=$.length+6;for(let G=0;G<W;G++){const O=Number(Y(await z.evaluate(Qz(J))));if(!Number.isFinite(O)||O<0||O<=q)break;await z.pressKey("Backspace");await z.wait({time:0.15})}}catch{}}}return{added:Q,failed:Z}}async function fz(z){const J=await z.evaluate(`
295
255
  () => {
296
256
  const isVisible = (el) => {
297
257
  if (!el || el.offsetParent === null) return false;
@@ -340,7 +300,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
340
300
  }
341
301
  return { ok: false, visibleTexts };
342
302
  }
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(`
303
+ `);if(J?.ok)await z.wait({time:1});return J}async function h(z,J,K=7000){const Q=500,Z=Math.max(1,Math.ceil(K/Q));let $={ok:!1};for(let q=0;q<Z;q++){$=Y(await z.evaluate(`
344
304
  ((cfg) => {
345
305
  const __opencli_xhs_click_label = { wantLabel: ${JSON.stringify(J)} };
346
306
  const wantLabel = ${JSON.stringify(J)};
@@ -375,7 +335,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
375
335
  }
376
336
  return { ok: false };
377
337
  })()
378
- `));if(q?.ok)return q;await z.wait({time:Q/1000})}return q}async function fz(z){const J=await z.evaluate(`
338
+ `));if($?.ok)return $;await z.wait({time:Q/1000})}return $}async function Az(z){const J=await z.evaluate(`
379
339
  (() => {
380
340
  const __opencli_xhs_focus_card = true;
381
341
  const sel = ${JSON.stringify(u)};
@@ -392,7 +352,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
392
352
  selection?.addRange(range);
393
353
  return { ok: true };
394
354
  })()
395
- `);return W(J)}async function Az(z){const J=await z.evaluate(`
355
+ `);return Y(J)}async function Rz(z){const J=await z.evaluate(`
396
356
  (() => {
397
357
  const __opencli_xhs_card_text = true;
398
358
  const sel = ${JSON.stringify(u)};
@@ -401,7 +361,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
401
361
  const text = active ? (active.innerText || active.textContent || '').trim() : '';
402
362
  return { ok: !!text, text };
403
363
  })()
404
- `);return W(J)}async function $z(z){const J=await z.evaluate(`
364
+ `);return Y(J)}async function Zz(z){const J=await z.evaluate(`
405
365
  (() => {
406
366
  const __opencli_xhs_card_count = true;
407
367
  const sel = ${JSON.stringify(u)};
@@ -410,16 +370,16 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
410
370
  const activeText = active ? (active.innerText || active.textContent || '').trim() : '';
411
371
  return { ok: true, count: editors.length, activeEmpty: !activeText };
412
372
  })()
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(`
373
+ `);return Y(J)}async function yz(z,J=8000){const K=300,Q=Math.ceil(J/K);for(let Z=0;Z<Q;Z++){if((await Zz(z))?.count>=1)return!0;await z.wait({time:K/1000})}return!1}async function bz(z,J,K=6000){const Q=300,Z=Math.ceil(K/Q);for(let $=0;$<Z;$++){const q=await Zz(z);if(q?.count>=J&&q?.activeEmpty)return!0;await z.wait({time:Q/1000})}return!1}async function wz(z,J,K=4){for(let Q=0;Q<K;Q++){if(!(await h(z,g))?.ok)return!1;if(await bz(z,J,2500))return!0}return!1}async function Tz(z){const J=await z.evaluate(`
414
374
  (() => {
415
375
  const __opencli_xhs_preview_ready = true;
416
376
  const ready = Array.from(document.querySelectorAll('button'))
417
377
  .some((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '下一步');
418
378
  return { ok: ready };
419
379
  })()
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(`
380
+ `);return Y(J)}async function Cz(z,J=6){const K=400,Q=Math.ceil(3000/K);for(let Z=0;Z<J;Z++){if(!(await h(z,r))?.ok&&Z===0)return!1;for(let q=0;q<Q;q++){if((await Tz(z))?.ok)return!0;await z.wait({time:K/1000})}}return!1}async function Sz(z,J,K){J=String(J).replace(/\\n/g,`
381
+ `);if(!(await Az(z))?.ok)throw new j(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const $=J.split(`
382
+ `);for(let q=0;q<$.length;q++){if(q>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 Rz(z))?.ok)throw new j(`文字配图: card editor #${K+1} is empty after typing`)}async function xz(z,J){if(!J||J===I)return I;const K=Y(await z.evaluate(`
423
383
  (async () => {
424
384
  const __opencli_xhs_card_styles = true;
425
385
  const want = ${JSON.stringify(J)};
@@ -448,7 +408,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
448
408
  }
449
409
  return { ok: seen.length > 0, styles: seen, found: false };
450
410
  })()
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(`
411
+ `));if(!K?.found)throw new j(`文字配图: requested style "${J}" is not available for this content (options: ${(K?.styles||[]).join(" / ")||"none"}). Choose an available style or omit --card-style to use ${I}.`);if(!(await h(z,J))?.ok)throw new j(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function $z(z){const J=await z.evaluate(`
452
412
  (() => {
453
413
  const __opencli_xhs_composer_media_count = true;
454
414
  const visibleBox = (el) => {
@@ -480,7 +440,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
480
440
  }
481
441
  return { ok: true, count };
482
442
  })()
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(`
443
+ `);return Y(J)}async function qz(z,J,K){const Q=await $z(z);if(!Q||typeof Q.count!=="number")throw new j(`${K}: could not verify current composer media count`);if(Q.count<J){await z.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new j(`${K}: expected at least ${J} visible media item(s), got ${Q.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function Ez(z,J,K){if(!(await h(z,E))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new j(`文字配图: could not click "${E}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await yz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new j(`文字配图: 写文字 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 wz(z,V+1)){await z.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new j(`文字配图: new card editor #${V+1} did not render after "${g}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await Sz(z,J[V],V)}if(!await Cz(z)){await z.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new j(`文字配图: "${r}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const $=await xz(z,K);if(!(await h(z,p))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new j(`文字配图: could not click "${p}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return $}async function Vz(z){return z.evaluate(`
484
444
  () => {
485
445
  const text = (document.body?.innerText || '').replace(/s+/g, ' ').trim();
486
446
  const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
@@ -510,7 +470,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
510
470
  const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
511
471
  return { state, hasTitleInput, hasImageInput, hasVideoSurface };
512
472
  }
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(`
473
+ `)}async function uz(z,J=5000){const K=500,Q=Math.max(1,Math.ceil(J/K));let Z=await Vz(z);for(let $=0;$<Q;$++){if(Z.state!=="video_surface")return Z;if($<Q-1){await z.wait({time:K/1000});Z=await Vz(z)}}return Z}async function mz(z,J=1e4){const K=1000,Q=Math.ceil(J/K);for(let Z=0;Z<Q;Z++){if(await z.evaluate(`
514
474
  (() => {
515
475
  const sels = ${JSON.stringify(m)};
516
476
  for (const sel of sels) {
@@ -518,14 +478,14 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
518
478
  if (el && el.offsetParent !== null) return true;
519
479
  }
520
480
  return false;
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(`
481
+ })()`))return!0;if(Z<Q-1)await z.wait({time:K/1000})}return!1}async function cz(z){const J=Y(await z.evaluate(`
522
482
  (() => {
523
483
  const __opencli_xhs_restore_srcs = true;
524
484
  return Array.from(document.querySelectorAll('.img-container img'))
525
485
  .map((img) => img.currentSrc || img.src || '')
526
486
  .filter(Boolean);
527
487
  })()
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(`
488
+ `));return Array.isArray(J)?J:[]}async function Hz(z,J){if(!Array.isArray(J)||J.length===0)return 0;const K=Y(await z.evaluate(`
529
489
  (async (wanted) => {
530
490
  const __opencli_xhs_delete_by_src = true;
531
491
  const want = new Set(wanted);
@@ -556,7 +516,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
556
516
  }
557
517
  return removed;
558
518
  })(${JSON.stringify(J)})
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(`(() => {
519
+ `));return Number(K)||0}function dz(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 jz(z){return Math.min(45,6+z*4)}async function Gz(z,J,K,Q=90000){if(J<=0)return{ok:!0,completed:0};if(!K||typeof z.readNetworkCapture!=="function"){await z.wait({time:jz(J)});return{ok:!0,completed:null,viaFallback:!0}}const Z=1500,q=Math.ceil(9000/Z),V=Math.ceil(Q/Z),W=new Set;for(let G=0;G<V;G++){const O=await z.readNetworkCapture().catch(()=>[]);for(const D of Array.isArray(O)?O:[]){const X=dz(D);if(X)W.add(X)}if(W.size>=J)return{ok:!0,completed:W.size};if(G>=q&&W.size===0){await z.wait({time:jz(J)});return{ok:!0,completed:null,viaFallback:!0}}await z.wait({time:Z/1000})}return{ok:!1,completed:W.size}}async function nz(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 $;try{$=Y(await z.evaluate(`(() => {
560
520
  const noteIdRe = /"noteId":"([0-9a-f]{24})"/;
561
521
  return Array.from(document.querySelectorAll('div.note[data-impression], div.note')).slice(0, 3).map((card) => {
562
522
  const impression = card.getAttribute('data-impression') || '';
@@ -565,7 +525,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
565
525
  const dateText = (card.querySelector('.time_status, .time')?.innerText || '').trim();
566
526
  return { id, title, dateText };
567
527
  }).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(`
528
+ })()`))}catch{return null}if(Array.isArray($)){const q=$.find((V)=>{const W=String(V.title||"").trim();if(!(W===Q||W.startsWith(Q)||Q.startsWith(W)))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(q)return q}}return null}Wz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Fz.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:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${I}。可选: ${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 H=R.resolve(Q);if(!S.existsSync(H))throw new U(`--file 指向的文件不存在: ${H}`);Z=S.readFileSync(H,"utf-8").trim();if(!Z)throw new U(`--file 指向的文件内容为空: ${H}`)}const $=J.images?String(J.images).split(",").map((H)=>H.trim()).filter(Boolean):[],q=J.topics?String(J.topics).split(",").map((H)=>H.trim()).filter(Boolean):[],V=Boolean(J.draft),W=J["card-text"]?String(J["card-text"]):"",G=W?W.split(o).map((H)=>H.trim()).filter(Boolean):[],O=J["card-style"]?String(J["card-style"]).trim():"",D=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 <本机文件路径>");Oz(Z,{fileFlag:"--file"});if(!D&&$.length===0)throw new U("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if($.length>n)throw new U(`Too many images: ${$.length} (max ${n})`);if(D&&$.some((H)=>R.extname(H).toLowerCase()===".gif"))throw new U("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const X=Iz($);await z.goto(vz);await z.wait({time:1});await z.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let M="";for(let H=0;H<30;H++){await z.wait({time:0.5});M=await z.evaluate("() => location.href");if(M.includes("creator.xiaohongshu.com"))break}if(!M.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${M}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const L=await fz(z);if((await uz(z,L?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const H=L?.ok?`clicked "${L.text}"`:`visible candidates: ${(L?.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 _=await cz(z);let f=O;if(D)f=await Ez(z,G,O);else{const H=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,B=await e(z,X);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new j(`图片注入失败:${B.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await z.wait({time:i/1000});const v=await Gz(z,X.length,H);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new j(`图片上传未全部完成:只确认 ${v.completed}/${X.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await zz(z);if(_.length){await Hz(z,_);await z.wait({time:0.5})}const A=await $z(z);if(A&&typeof A.count==="number"&&A.count>X.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new j(`清掉恢复态旧图后编辑器内仍有 ${A.count} 张图,超过本次注入的 ${X.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await mz(z)){await z.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new j("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(D)await qz(z,G.length,"文字配图 generated images");if(D&&X.length>0){const H=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,B=await e(z,X);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new j(`追加图片失败:${B.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await z.wait({time:i/1000});const v=await Gz(z,X.length,H);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new j(`追加图片上传未全部完成:只确认 ${v.completed}/${X.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await zz(z);await qz(z,G.length+X.length,"文字配图 appended images")}if(D&&_.length){await Hz(z,_);await z.wait({time:0.5})}await Jz(z,m,K,"title");await N(z,0.6);await Jz(z,t,Z,"content");await N(z,0.6);let y=[],b=[];if(q.length){const H=await _z(z,t,q);y=H.added;b=H.failed}await N(z,1,0.6);const w=await Dz(z,["xhs-publish-btn"]);if(w){await Bz(z,w.x,w.y);await N(z,0.4)}const c=V?["暂存离开","存草稿"]:["发布","发布笔记"],F=await z.evaluate(`
569
529
  (cfg => {
570
530
  const { isDraftMode, publishNames, draftNames, labels } = cfg;
571
531
  const isVisible = (el) => {
@@ -604,7 +564,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
604
564
  }
605
565
  }
606
566
  return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
607
- })(${JSON.stringify({isDraftMode:V,publishNames:kz,draftNames:Nz,labels:c})})
567
+ })(${JSON.stringify({isDraftMode:V,publishNames:Pz,draftNames:kz,labels:c})})
608
568
  `);if(!F?.ok){if(V){if(await z.evaluate(`
609
569
  (() => {
610
570
  const labels = ['返回', '关闭', '取消', '离开'];
@@ -643,7 +603,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
643
603
  }
644
604
  return false;
645
605
  }
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(`
606
+ `)){F.ok=!0;F.via="auto-save"}}}}if(!F?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const H=F?.via?` (via=${F.via})`:"",B=F?.error?`, error=${F.error}`:"",v=F?.lastMethodError?`, lastMethodError=${F.lastMethodError}`:"";throw Error(`未能触发「${c[0]}」按钮${H}${B}${v}。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_submit_debug.png")}await z.wait({time:4});const k=await z.evaluate("() => location.href"),Xz=V?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],d=await z.evaluate(`
647
607
  (markers => {
648
608
  for (const el of document.querySelectorAll('*')) {
649
609
  if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
@@ -653,4 +613,4 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
653
613
  }
654
614
  return '';
655
615
  })(${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}`:""}]}});
616
+ `),Yz=!k.includes("/publish/publish");let T=d.length>0||Yz;const C=V?"暂存成功":"发布成功";let P=null;if(!V){P=await nz(z,K,T?4:8);if(P)T=!0}if(!T){if(V)throw new j(`${C}无法确认:未观察到成功提示或提交后跳转。`+(k?`当前 URL: ${k}。`:"当前 URL 为空。")+"先用 `xiaohongshu drafts` 查草稿箱确认是否已存上;确认没有再重新运行本命令,避免堆出重复草稿。");throw new j(`${C}无法确认:未观察到成功提示或提交后跳转,创作者后台也暂未查到同标题笔记。`+(k?`当前 URL: ${k}。`:"")+"注意:发布按钮已被触发,笔记仍可能延迟入库。禁止立即重发——请等 1-2 分钟后用 `xiaohongshu creator-notes` 回查本篇标题是否已出现;确认不存在后才重新运行本 publish 命令,否则会重复发布。")}return[{status:`✅ ${C}`,detail:[`"${K}"`,D?`${G.length}张文字配图${X.length?` + ${X.length}张图片`:""}${f&&f!==I?` (${f})`:""}`:`${X.length}张图片`,y.length?`话题: ${y.join(" ")}`:"",b.length?`⚠️ 话题未附上(已自动跳过,不影响发布): ${b.map((H)=>H.topic).join(" ")}`:"",P?.id?`笔记ID: ${P.id}`:"",d||(P?"已在创作者后台确认":"")||k||""].filter(Boolean).join(" · "),url:P?.id?`https://www.xiaohongshu.com/explore/${P.id}`:""}]}});
@@ -9,6 +9,6 @@ export declare function waitForBridgeReady(fetchHealth: HealthFetcher, opts: {
9
9
  contextId?: string;
10
10
  intervalMs?: number;
11
11
  }): Promise<DaemonHealth>;
12
- export declare const PRE_DISPATCH_ERROR_CODES: Set<"extension_not_connected" | "profile_disconnected">;
12
+ export declare const PRE_DISPATCH_ERROR_CODES: Set<"profile_disconnected" | "extension_not_connected">;
13
13
  export type PreDispatchErrorCode = typeof PRE_DISPATCH_ERROR_CODES extends Set<infer T> ? T : never;
14
14
  export declare function isPreDispatchError(errorCode: string | undefined): boolean;
@@ -4,9 +4,57 @@
4
4
  * Provides a typed send() function that posts a Command and returns a Result.
5
5
  */
6
6
  import { fetchDaemonStatus, getDaemonHealth, requestDaemonShutdown, type BrowserProfileStatus, type DaemonHealth, type DaemonStatus } from './daemon-transport.js';
7
+ /**
8
+ * Id for a whole logical CLI command run. Encodes the CLI pid so the daemon can
9
+ * name/kill the holder in a busy error (see parsePidFromRunId in session-lease.ts).
10
+ */
11
+ export declare function generateRunId(): string;
12
+ /**
13
+ * Identity of the CLI command run currently driving the browser, attached to
14
+ * every command so the daemon can arbitrate the write lease. Module-level and
15
+ * one-per-invocation, matching `setDaemonCommandTimeoutSeconds`. Null for read
16
+ * and ephemeral commands, which are never lease-arbitrated.
17
+ */
18
+ export interface DaemonRunContext {
19
+ runId: string;
20
+ command: string;
21
+ access: 'read' | 'write';
22
+ }
23
+ export declare function setDaemonRunContext(ctx: DaemonRunContext | null): void;
24
+ /**
25
+ * Clear the run context only if it still belongs to `runId`. Used by deferred
26
+ * cleanup (an adapter that outlived its CLI timeout settles later): by then a
27
+ * newer run may own the context, and unconditionally nulling it would strip
28
+ * that run's lease heartbeats mid-flight.
29
+ */
30
+ export declare function clearDaemonRunContext(runId: string): void;
31
+ /**
32
+ * Best-effort release of a persistent site-session lease on command completion.
33
+ * A direct one-shot POST (no bridge ensure / retry): if it never lands, the
34
+ * daemon's TTL reclaims the lease anyway, so this must never block the caller.
35
+ */
36
+ export declare function releaseSiteSessionLease(params: {
37
+ runId: string;
38
+ session: string;
39
+ surface: 'adapter';
40
+ }): Promise<void>;
41
+ /**
42
+ * Propagate the user's `--timeout` down to the transport layer. Without this
43
+ * the daemon/HTTP deadlines stay at their defaults and a long-running command
44
+ * gets aborted mid-flight even though the user explicitly allowed more time.
45
+ */
46
+ export declare function setDaemonCommandTimeoutSeconds(seconds: number | null): void;
47
+ /**
48
+ * True when a thrown error carries an unknown-outcome code — the browser-side
49
+ * command may still be running even though the client gave up. Callers use this
50
+ * to decide whether it is safe to release a persistent site-session lease: it is
51
+ * not, because the still-running command keeps mutating the tab. Walks the cause
52
+ * chain so a wrapped `BrowserCommandError` is still recognized.
53
+ */
54
+ export declare function isUnknownOutcomeError(err: unknown): boolean;
7
55
  export interface DaemonCommand {
8
56
  id: string;
9
- action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'wait-download' | 'cdp' | 'frames';
57
+ action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'wait-download' | 'cdp' | 'frames' | 'lease-release';
10
58
  /** Target page identity (targetId). Cross-layer contract with the extension. */
11
59
  page?: string;
12
60
  code?: string;
@@ -45,6 +93,29 @@ export interface DaemonCommand {
45
93
  frameIndex?: number;
46
94
  /** Browser profile/context to route the command to. */
47
95
  contextId?: string;
96
+ /**
97
+ * Daemon-side command timeout in seconds. Set by the transport layer from
98
+ * the effective command deadline; kept for older daemons — new code prefers
99
+ * `deadlineAt`.
100
+ */
101
+ timeout?: number;
102
+ /**
103
+ * Absolute command deadline (epoch ms). All hops run on one machine, so
104
+ * every layer derives its remaining budget as `deadlineAt - Date.now()`,
105
+ * absorbing queueing and service-worker wake latency.
106
+ */
107
+ deadlineAt?: number;
108
+ /**
109
+ * Stable id for the whole logical CLI command run (NOT the per-exec `id`).
110
+ * The daemon uses it to arbitrate a write lease on the persistent site
111
+ * session: the first command acquires, same-runId execs refresh it as a
112
+ * heartbeat, a concurrent different-runId write fails fast. See session-lease.ts.
113
+ */
114
+ runId?: string;
115
+ /** Human command name (e.g. `chatgpt ask`) surfaced in the busy error. */
116
+ command?: string;
117
+ /** Command access level; only 'write' commands take/hold a session lease. */
118
+ access?: 'read' | 'write';
48
119
  }
49
120
  export interface DaemonResult {
50
121
  id: string;
@@ -1 +1 @@
1
- import{sleep as L}from"../utils.js";import{BrowserConnectError as D}from"../errors.js";import{classifyBrowserError as S}from"./errors.js";import{resolveProfileContextId as M}from"./profile.js";import{DEFAULT_BROWSER_CONNECT_TIMEOUT as $}from"./config.js";import{ensureBrowserBridgeReady as H}from"./daemon-lifecycle.js";import{isPreDispatchError as O}from"./bridge-readiness.js";import{fetchDaemonStatus,getDaemonHealth,requestDaemon as _,requestDaemonShutdown}from"./daemon-transport.js";let g=0;function N(){return`cmd_${process.pid}_${Date.now()}_${++g}`}export class BrowserCommandError extends Error{code;hint;constructor(K,J,Q){super(K);this.code=J;this.hint=Q;this.name="BrowserCommandError"}}export{fetchDaemonStatus,getDaemonHealth,requestDaemonShutdown};async function f(K,J){const Q=4;let X=!1,P=!1,Y=!1;const b=K==="navigate";let Z=!1;for(let F=1;F<=Q;F++){const j=N(),q=process.env.OPENCLI_WINDOW,x=q==="foreground"||q==="background"?q:void 0,V=J.contextId??M(),k=J.windowMode??x,W={id:j,action:K,...J,...V&&{contextId:V},...k&&{windowMode:k}};try{const G=await _("/command",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(W),timeout:30000}),z=await G.json();if(z.ok)return z;if(z.errorCode==="command_result_unknown"){if(b&&!Z){Z=!0;await H({timeoutSeconds:$,contextId:V,verbose:!1});continue}throw new BrowserCommandError(z.error??"Browser command result is unknown",z.errorCode,z.errorHint)}if(!X&&O(z.errorCode)){X=!0;await H({timeoutSeconds:$,contextId:V,verbose:!1});continue}if(G.status===409&&!z.errorCode&&(z.error??"").includes("Duplicate command id")&&!P){P=!0;continue}const A=S(Error(z.error??""));if(A.retryable&&!Y){Y=!0;await L(A.delayMs);continue}throw new BrowserCommandError(z.error??"Daemon command failed",z.errorCode,z.errorHint)}catch(G){if(G instanceof BrowserCommandError||G instanceof D)throw G;if(G instanceof Error&&G.name==="AbortError"){if(b&&!Z){Z=!0;await H({timeoutSeconds:$,contextId:V,verbose:!1});continue}throw new BrowserCommandError("Browser command timed out client-side; the page may still have applied it.","command_result_unknown","Inspect the page state before retrying. Idempotent reads are safe to retry; non-idempotent writes may have already happened.")}if(!X&&G instanceof TypeError){X=!0;await H({timeoutSeconds:$,contextId:V,verbose:!1});continue}if(G instanceof Error){const z=S(G);if(z.retryable&&!Y){Y=!0;await L(z.delayMs);continue}}throw G}}throw new BrowserCommandError("sendCommand: max attempts exhausted","max_attempts_exhausted")}export async function sendCommand(K,J={}){return(await f(K,J)).data}export async function sendCommandFull(K,J={}){const Q=await f(K,J);return{data:Q.data,page:Q.page}}export async function bindTab(K,J={}){return sendCommand("bind",{session:K,surface:"browser",...J})}
1
+ import{sleep as U}from"../utils.js";import{BrowserConnectError as p,SessionBusyError as x}from"../errors.js";import{COMMAND_RESULT_UNKNOWN_CODE as q,COMMAND_RESULT_UNKNOWN_HINT as P}from"../daemon-utils.js";import{classifyBrowserError as w}from"./errors.js";import{resolveProfileContextId as M}from"./profile.js";import{DEFAULT_BROWSER_CONNECT_TIMEOUT as T}from"./config.js";import{ensureBrowserBridgeReady as l}from"./daemon-lifecycle.js";import{isPreDispatchError as O}from"./bridge-readiness.js";import{fetchDaemonStatus,getDaemonHealth,requestDaemon as g,requestDaemonShutdown}from"./daemon-transport.js";let A=0;function f(){return`cmd_${process.pid}_${Date.now()}_${++A}`}export function generateRunId(){return`run_${process.pid}_${Date.now()}_${++A}`}let F=null;export function setDaemonRunContext(z){F=z}export function clearDaemonRunContext(z){if(F?.runId===z)F=null}export async function releaseSiteSessionLease(z){try{await g("/command",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:f(),action:"lease-release",runId:z.runId,session:z.session,surface:z.surface}),timeout:2000})}catch{}}const E=120,C=15000,_=1e4;let D=null;export function setDaemonCommandTimeoutSeconds(z){D=typeof z==="number"&&z>0?Math.ceil(z):null}function N(z){const Q=D??E;if(typeof z.timeoutMs==="number"&&z.timeoutMs>0)return Math.max(Q,Math.ceil((z.timeoutMs+C)/1000));return Q}const c="1.1.0";function i(z,Q){if(!z)return!1;const Y=z.replace(/^v/,"").split("-")[0].split(".").map(Number),$=Q.split(".").map(Number);if(Y.length<3||Y.some(Number.isNaN))return!1;for(let V=0;V<3;V++){const k=Y[V]-($[V]??0);if(k!==0)return k>0}return!0}const I=new Set(["command_result_unknown","command_lost","result_evicted"]);export function isUnknownOutcomeError(z){const Q=new Set;let Y=z;while(Y&&typeof Y==="object"&&!Q.has(Y)){Q.add(Y);const $=Y.code;if(typeof $==="string"&&I.has($))return!0;Y=Y.cause}return!1}const u=4,m=new Set(["ECONNREFUSED","UND_ERR_CONNECT_TIMEOUT","EHOSTUNREACH","ENETUNREACH","ENOTFOUND"]);function d(z){const Q=[z],Y=new Set;while(Q.length){const $=Q.pop();if(!$||typeof $!=="object"||Y.has($))continue;Y.add($);const{code:V,cause:k,errors:X}=$;if(typeof V==="string"&&m.has(V))return!0;if(k)Q.push(k);if(Array.isArray(X))Q.push(...X)}return!1}export class BrowserCommandError extends Error{code;hint;constructor(z,Q,Y){super(z);this.code=Q;this.hint=Y;this.name="BrowserCommandError"}}export{fetchDaemonStatus,getDaemonHealth,requestDaemonShutdown};async function R(z,Q){const Y=N(Q),$=Date.now()+Y*1000,V=process.env.OPENCLI_WINDOW,k=V==="foreground"||V==="background"?V:void 0,X=Q.contextId??M(),y=Q.windowMode??k,B=z==="navigate";let v=!1,j=f(),b=!1,L=!1,K=null;const H=async()=>{const J=Math.ceil(($-Date.now())/1000),h=await l({timeoutSeconds:Math.max(1,Math.min(T,J)),contextId:X,verbose:!1});K=i(h.health.status?.extensionVersion,c)};for(let J=1;J<=u;J++){if(J>1&&Date.now()>=$)throw new BrowserCommandError("Browser command deadline exhausted across transport retries.",q,P);const h=Math.max(1000,$-Date.now()),S={id:j,action:z,...Q,timeout:Y,deadlineAt:$,...X&&{contextId:X},...y&&{windowMode:y},...F&&{runId:F.runId,command:F.command,access:F.access}};try{const Z=await(await g("/command",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(S),timeout:h+_})).json();if(Z.ok)return Z;if(Z.errorCode==="session_busy")throw new x(Z.error??"The site session is busy.",Z.errorHint);if(Z.errorCode&&I.has(Z.errorCode)){if(B&&!v){v=!0;j=f();await H();continue}throw new BrowserCommandError(Z.error??"Browser command result is unknown",Z.errorCode,Z.errorHint)}if(O(Z.errorCode)&&!b){b=!0;await H();continue}if(Z.errorCode==="daemon_shutting_down"&&!b){b=!0;await H();if(K)continue;throw new BrowserCommandError(Z.error??"Daemon shut down mid-command; the command may have already been applied.",q,P)}const W=w(new BrowserCommandError(Z.error??"",Z.errorCode));if(W.kind==="extension-transient"&&!L){L=!0;j=f();await U(W.delayMs);continue}throw new BrowserCommandError(Z.error??"Daemon command failed",Z.errorCode,Z.errorHint)}catch(G){if(G instanceof BrowserCommandError||G instanceof p||G instanceof x)throw G;if(G instanceof Error&&G.name==="AbortError")throw new BrowserCommandError("Browser command timed out client-side; the page may still have applied it.",q,P);if(G instanceof TypeError){await H();if(K||d(G))continue;throw new BrowserCommandError("Connection to the daemon was lost mid-command; it may have already been applied.",q,P)}throw G}}throw new BrowserCommandError("sendCommand: max attempts exhausted","max_attempts_exhausted")}export async function sendCommand(z,Q={}){return(await R(z,Q)).data}export async function sendCommandFull(z,Q={}){const Y=await R(z,Q);return{data:Y.data,page:Y.page}}export async function bindTab(z,Q={}){return sendCommand("bind",{session:z,surface:"browser",...Q})}
@@ -1,4 +1,4 @@
1
- import type { DaemonStatus } from './daemon-client.js';
1
+ import type { DaemonStatus } from './daemon-transport.js';
2
2
  export declare function isDaemonStale(status: Pick<DaemonStatus, 'daemonVersion'> | null | undefined, cliVersion?: string): boolean;
3
3
  export declare function formatDaemonVersion(status: Pick<DaemonStatus, 'daemonVersion'> | null | undefined): string;
4
4
  export declare function staleDaemonIssue(status: Pick<DaemonStatus, 'daemonVersion'> | null | undefined, cliVersion: string): string;
@@ -27,6 +27,8 @@ export interface RetryAdvice {
27
27
  * Classify a browser error and return retry advice.
28
28
  *
29
29
  * Single source of truth for "is this error transient?" across all layers.
30
+ * Prefers the machine-readable `code` carried by BrowserCommandError; falls
31
+ * back to message patterns for legacy extensions.
30
32
  */
31
33
  export declare function classifyBrowserError(err: unknown): RetryAdvice;
32
34
  /**
@@ -1,5 +1,5 @@
1
- import{BrowserConnectError as z}from"../errors.js";import{DEFAULT_DAEMON_PORT as J}from"../constants.js";const K=["Extension disconnected","Extension not connected","attach failed","Detached while handling command","Debugger is not attached to the tab","no longer exists","No tab with id","CDP connection","Daemon command failed","No window with id"],Q=["Inspected target navigated or closed"];function W(q){return q instanceof Error?q.message:String(q)}export function classifyBrowserError(q){const x=W(q);if(K.some((H)=>x.includes(H)))return{kind:"extension-transient",retryable:!0,delayMs:1500};if(Q.some((H)=>x.includes(H)))return{kind:"target-navigation",retryable:!0,delayMs:200};if(x.includes("-32000")&&/target|context/i.test(x))return{kind:"target-navigation",retryable:!0,delayMs:200};return{kind:"non-retryable",retryable:!1,delayMs:0}}export function isTransientBrowserError(q){return classifyBrowserError(q).retryable}export function formatBrowserConnectError(q,x){switch(q){case"daemon-not-running":return new z("Cannot connect to ppcli daemon."+(x?`
1
+ import{BrowserConnectError as H}from"../errors.js";import{DEFAULT_DAEMON_PORT as Q}from"../constants.js";const W=["Extension disconnected","Extension not connected","attach failed","Detached while handling command","Debugger is not attached to the tab","no longer exists","No tab with id","CDP connection","Daemon command failed","No window with id"],Y=["Inspected target navigated or closed"];function Z(q){return q instanceof Error?q.message:String(q)}const K={attach_failed:{kind:"extension-transient",retryable:!0,delayMs:1500},tab_gone:{kind:"extension-transient",retryable:!0,delayMs:1500},target_navigated:{kind:"target-navigation",retryable:!0,delayMs:200},detached_mid_command:{kind:"non-retryable",retryable:!1,delayMs:0},cdp_timeout:{kind:"non-retryable",retryable:!1,delayMs:0}};export function classifyBrowserError(q){const x=q&&typeof q==="object"?q.code:void 0;if(typeof x==="string"&&K[x])return K[x];const z=Z(q);if(W.some((J)=>z.includes(J)))return{kind:"extension-transient",retryable:!0,delayMs:1500};if(Y.some((J)=>z.includes(J)))return{kind:"target-navigation",retryable:!0,delayMs:200};if(z.includes("-32000")&&/target|context/i.test(z))return{kind:"target-navigation",retryable:!0,delayMs:200};return{kind:"non-retryable",retryable:!1,delayMs:0}}export function isTransientBrowserError(q){return classifyBrowserError(q).retryable}export function formatBrowserConnectError(q,x){switch(q){case"daemon-not-running":return new H("Cannot connect to ppcli daemon."+(x?`
2
2
 
3
- ${x}`:""),`Run \`ppcli doctor\` to diagnose, or \`ppcli daemon restart\` to force a fresh daemon. Default port is ${J}.`,q);case"extension-not-connected":return new z("Browser Bridge extension is not connected."+(x?`
3
+ ${x}`:""),`Run \`ppcli doctor\` to diagnose, or \`ppcli daemon restart\` to force a fresh daemon. Default port is ${Q}.`,q);case"extension-not-connected":return new H("Browser Bridge extension is not connected."+(x?`
4
4
 
5
- ${x}`:""),"Install the extension from GitHub Releases, then reload.",q);case"command-failed":return new z(`Browser command failed: ${x??"unknown error"}`,void 0,q);default:return new z(x??"Failed to connect to browser",void 0,q)}}
5
+ ${x}`:""),"Install the extension from GitHub Releases, then reload.",q);case"command-failed":return new H(`Browser command failed: ${x??"unknown error"}`,void 0,q);default:return new H(x??"Failed to connect to browser",void 0,q)}}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Injected page-side network interceptor.
3
+ *
4
+ * Used when the session-level capture channel (CDP/extension) is unavailable.
5
+ * It captures fetch/XHR response bodies while matching the CDP path's
6
+ * truncation contract: bodies above the per-entry cap are stored as a string
7
+ * prefix with `bodyTruncated: true` and `bodyFullSize` set.
8
+ *
9
+ * Keep this script dependency-free; it executes in the target page context.
10
+ */
11
+ export declare const NETWORK_INTERCEPTOR_JS = "(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=1048576,F=window.fetch;function capture(url,method,status,text,ct){if(window.__opencli_net.length>=M)return;var full=text?text.length:0,trunc=full>B,stored=trunc?text.slice(0,B):text,body=null;if(stored){if(trunc){body=stored}else{try{body=JSON.parse(stored)}catch(e){body=stored}}}var e={url:url,method:method||'GET',status:status,size:full,ct:ct,body:body,timestamp:Date.now()};if(trunc){e.bodyTruncated=true;e.bodyFullSize=full}window.__opencli_net.push(e)}window.fetch=async function(){var r=await F.apply(this,arguments);try{var ct=r.headers.get('content-type')||'';if(ct.includes('json')||ct.includes('text')){var c=r.clone(),t=await c.text();capture(r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),(arguments[1]&&arguments[1].method)||'GET',r.status,t,ct)}}catch(e){}return r};var X=XMLHttpRequest.prototype,O=X.open,S=X.send;X.open=function(m,u){this._om=m;this._ou=u;return O.apply(this,arguments)};X.send=function(){var x=this;x.addEventListener('load',function(){try{var ct=x.getResponseHeader('content-type')||'';if(ct.includes('json')||ct.includes('text')){capture(x._ou,x._om||'GET',x.status,x.responseText||'',ct)}}catch(e){}});return S.apply(this,arguments)}})()";
@@ -0,0 +1 @@
1
+ export const NETWORK_INTERCEPTOR_JS="(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=1048576,F=window.fetch;function capture(url,method,status,text,ct){if(window.__opencli_net.length>=M)return;var full=text?text.length:0,trunc=full>B,stored=trunc?text.slice(0,B):text,body=null;if(stored){if(trunc){body=stored}else{try{body=JSON.parse(stored)}catch(e){body=stored}}}var e={url:url,method:method||'GET',status:status,size:full,ct:ct,body:body,timestamp:Date.now()};if(trunc){e.bodyTruncated=true;e.bodyFullSize=full}window.__opencli_net.push(e)}window.fetch=async function(){var r=await F.apply(this,arguments);try{var ct=r.headers.get('content-type')||'';if(ct.includes('json')||ct.includes('text')){var c=r.clone(),t=await c.text();capture(r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),(arguments[1]&&arguments[1].method)||'GET',r.status,t,ct)}}catch(e){}return r};var X=XMLHttpRequest.prototype,O=X.open,S=X.send;X.open=function(m,u){this._om=m;this._ou=u;return O.apply(this,arguments)};X.send=function(){var x=this;x.addEventListener('load',function(){try{var ct=x.getResponseHeader('content-type')||'';if(ct.includes('json')||ct.includes('text')){capture(x._ou,x._om||'GET',x.status,x.responseText||'',ct)}}catch(e){}});return S.apply(this,arguments)}})()";