publishport-opencli 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-manifest.json +445 -3
- package/clis/facebook/feed.js +78 -5
- package/clis/facebook/search.js +83 -22
- package/clis/instagram/_shared/private-publish.js +4 -4
- package/clis/instagram/post.js +20 -20
- package/clis/xianyu/collect.js +1 -0
- package/clis/xianyu/location.js +1 -0
- package/clis/xianyu/manage.js +1 -0
- package/clis/xianyu/mtop.js +47 -0
- package/clis/xianyu/order.js +1 -0
- package/clis/xianyu/rate.js +1 -0
- package/clis/xianyu/user.js +1 -0
- package/clis/xiaohongshu/publish.js +63 -44
- package/dist/src/browser/base-page.d.ts +19 -0
- package/dist/src/browser/base-page.js +1 -1
- package/dist/src/browser/target-resolver.d.ts +1 -0
- package/dist/src/browser/target-resolver.js +100 -6
- package/dist/src/cli.js +1 -1
- package/dist/src/external.js +1 -1
- package/dist/src/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import*as
|
|
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(
|
|
3
|
+
const sels = ${JSON.stringify(e)};
|
|
4
4
|
return !!document.querySelector(sels);
|
|
5
5
|
})()
|
|
6
|
-
`))return!0;if(Q
|
|
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 _ 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
|
|
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
|
|
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 _ 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(!
|
|
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 _ 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(
|
|
122
|
-
`);let $;if(
|
|
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 _ 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(
|
|
149
|
-
`))?.ok){await z.screenshot({path:`/tmp/xhs_publish_${
|
|
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 _ 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(
|
|
172
|
-
`)}catch{$=await q()}}else $=await q();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${
|
|
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 _ 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
|
|
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 _ 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
|
|
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 _ 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
|
|
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 _ 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
|
|
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
|
-
`))}
|
|
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 _ 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 L(z,J,K=7000){const
|
|
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 _ 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:
|
|
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 _ 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
|
|
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
|
|
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
|
|
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
|
|
411
|
-
`);if(!(await
|
|
412
|
-
`);for(let $=0;$<q.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(q[$])await
|
|
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 _ 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
|
|
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 _ 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(
|
|
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 _ 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
|
|
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 _ 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
|
|
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(
|
|
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
|
|
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
|
|
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 _ 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
|
|
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 _ 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:
|
|
589
|
-
`);if(!
|
|
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 _ 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
|
|
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 _ from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
614
633
|
}
|
|
615
634
|
}
|
|
616
635
|
return false;
|
|
617
|
-
})(${JSON.stringify(
|
|
618
|
-
`)){
|
|
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 _ from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
624
643
|
}
|
|
625
644
|
return false;
|
|
626
645
|
}
|
|
627
|
-
`)){
|
|
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 _ 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(
|
|
637
|
-
`),
|
|
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}`:""}]}});
|
|
@@ -18,6 +18,25 @@ export interface ResolveSuccess {
|
|
|
18
18
|
* clean `exact` match — the page changed, the action still succeeded.
|
|
19
19
|
*/
|
|
20
20
|
match_level: TargetMatchLevel;
|
|
21
|
+
/**
|
|
22
|
+
* Which physical click path executed: `cdp` (trusted Input.dispatchMouseEvent),
|
|
23
|
+
* `js` (el.click() fallback), or `ax` (accessibility-node click). Surfaced so
|
|
24
|
+
* agents can tell a trusted synthetic click from the untrusted JS fallback.
|
|
25
|
+
*/
|
|
26
|
+
click_method?: 'cdp' | 'js' | 'ax';
|
|
27
|
+
/**
|
|
28
|
+
* Result of hit-testing the click point against the resolved element:
|
|
29
|
+
* `target` (the point lands on the element or a descendant — trustworthy),
|
|
30
|
+
* `other` (an overlay/sibling covers it — the CDP click would miss), or
|
|
31
|
+
* `none` (nothing at the point). Only set on click().
|
|
32
|
+
*/
|
|
33
|
+
hit?: 'target' | 'ancestor' | 'other' | 'none';
|
|
34
|
+
/**
|
|
35
|
+
* True when the click was retargeted from the resolved element to a nearby
|
|
36
|
+
* clickable ancestor (e.g. an <svg> icon whose click handler lives on the
|
|
37
|
+
* wrapping <div>), so the handler actually fires. See issue #2071.
|
|
38
|
+
*/
|
|
39
|
+
retargeted?: boolean;
|
|
21
40
|
}
|
|
22
41
|
export interface FillTextResult extends ResolveSuccess {
|
|
23
42
|
filled: boolean;
|
|
@@ -44,7 +44,7 @@ ${G}
|
|
|
44
44
|
clearTimeout(timer);
|
|
45
45
|
}
|
|
46
46
|
})()
|
|
47
|
-
`,{request:W}),Z=Q.url||G;if(Q.error)throw new M("FETCH_ERROR",`Browser fetch failed for ${Z}: ${Q.error}`,"Check that the page is reachable and the current browser profile has access.");if(!Q.ok)throw new M("FETCH_ERROR",`HTTP ${Q.status??0}${Q.statusText?` ${Q.statusText}`:""} from ${Z}`,A(Q.text));const _=Q.text??"";if(!_.trim())return null;try{return JSON.parse(_)}catch{const $=Q.contentType?` (${Q.contentType})`:"";throw new M("FETCH_ERROR",`Expected JSON from ${Z}${$}`,A(_))}}async annotatedScreenshot(G={}){await this.snapshot({source:"dom",viewportExpand:0});try{await this.evaluate(m());return await this.screenshot({...G,annotate:!1})}finally{await this.evaluate(u()).catch(()=>{})}}async click(G,H={}){const W=await this.tryClickAxRef(G);if(W)return W;const Q=await Y(this,G,H),Z=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded"),_=await this.evaluate(P({skipScroll:Z}));if(_?.visible===!0){if(await this.tryNativeClick(_.x,_.y))return
|
|
47
|
+
`,{request:W}),Z=Q.url||G;if(Q.error)throw new M("FETCH_ERROR",`Browser fetch failed for ${Z}: ${Q.error}`,"Check that the page is reachable and the current browser profile has access.");if(!Q.ok)throw new M("FETCH_ERROR",`HTTP ${Q.status??0}${Q.statusText?` ${Q.statusText}`:""} from ${Z}`,A(Q.text));const _=Q.text??"";if(!_.trim())return null;try{return JSON.parse(_)}catch{const $=Q.contentType?` (${Q.contentType})`:"";throw new M("FETCH_ERROR",`Expected JSON from ${Z}${$}`,A(_))}}async annotatedScreenshot(G={}){await this.snapshot({source:"dom",viewportExpand:0});try{await this.evaluate(m());return await this.screenshot({...G,annotate:!1})}finally{await this.evaluate(u()).catch(()=>{})}}async click(G,H={}){const W=await this.tryClickAxRef(G);if(W)return W;const Q=await Y(this,G,H),Z=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded"),_=await this.evaluate(P({skipScroll:Z,forClick:!0})),$={hit:_?.hit,retargeted:_?.retargeted};if(_?.visible===!0&&(_.hit==="target"||_.hit==="ancestor")){if(await this.tryNativeClick(_.x,_.y))return{...Q,click_method:"cdp",...$}}const D=await this.evaluate(x({skipScroll:Z}));if(typeof D==="string"||D==null)return{...Q,click_method:"js",...$};if(D.status==="clicked")return{...Q,click_method:"js",...$};if(D.x!=null&&D.y!=null){if(await this.tryNativeClick(D.x,D.y))return{...Q,click_method:"cdp",...$}}throw Error(`Click failed: ${D.error??"JS click and CDP fallback both failed"}`)}async tryNativeClick(G,H){const W=this.nativeClick;if(typeof W!=="function")return!1;try{await W.call(this,G,H);return!0}catch{return!1}}async tryNativeMouseMove(G,H){const W=this.cdp;if(typeof W!=="function")return!1;try{await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:H});return!0}catch{return!1}}async tryNativeDoubleClick(G,H){const W=this.cdp;if(typeof W!=="function")return!1;try{await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:H});await W.call(this,"Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:H,button:"left",clickCount:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:H,button:"left",clickCount:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:H,button:"left",clickCount:2});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:H,button:"left",clickCount:2});return!0}catch{return!1}}async tryNativeDrag(G,H){const W=this.cdp;if(typeof W!=="function")return!1;const Q=Math.round((G.x+H.x)/2),Z=Math.round((G.y+H.y)/2);try{await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:G.x,y:G.y});await W.call(this,"Input.dispatchMouseEvent",{type:"mousePressed",x:G.x,y:G.y,button:"left",clickCount:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:Q,y:Z,button:"left",buttons:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:H.x,y:H.y,button:"left",buttons:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseReleased",x:H.x,y:H.y,button:"left",clickCount:1});return!0}catch{return!1}}async tryClickAxRef(G){if(!/^\d+$/.test(G))return null;const H=this._axRefs.get(G);if(!H)return null;const W=this.nativeClick;if(typeof W!=="function")return null;const Q=await this.resolveAxRefPoint(H);if(!Q)return null;try{await W.call(this,Q.x,Q.y);return{matches_n:1,match_level:Q.matchLevel,click_method:"ax"}}catch{return null}}async resolveAxRefPoint(G){const H=this.cdp;if(typeof H!=="function")return null;if(G.backendNodeId!=null){const _=await this.axBoxCenter(G.backendNodeId,G.frame).catch(()=>null);if(_)return{..._,matchLevel:"exact"}}await H.call(this,"Accessibility.enable",C(G.frame));const W=await H.call(this,"Accessibility.getFullAXTree",j(G.frame)).catch(()=>null),Q=w(W,G);if(!Q?.backendNodeId)return null;this._axRefs.set(G.ref,Q);const Z=await this.axBoxCenter(Q.backendNodeId,Q.frame).catch(()=>null);return Z?{...Z,matchLevel:"reidentified"}:null}async axBoxCenter(G,H){const W=this.cdp;if(typeof W!=="function")return null;const Q=await W.call(this,"DOM.getBoxModel",{backendNodeId:G,...H?.sessionId?{frameId:H.frameId,sessionId:H.sessionId,...H.targetUrl?{targetUrl:H.targetUrl}:{}}:{}}),Z=Array.isArray(Q?.model?.content)&&Q.model.content.length>=8?Q.model.content:Array.isArray(Q?.model?.border)&&Q.model.border.length>=8?Q.model.border:null;if(!Z)return null;const _=Z.slice(0,8).map(($)=>typeof $==="number"?$:Number($));if(_.some(($)=>!Number.isFinite($)))return null;return{x:Math.round((_[0]+_[2]+_[4]+_[6])/4),y:Math.round((_[1]+_[3]+_[5]+_[7])/4)}}async tryNativeType(G){const H=this.nativeType;if(typeof H==="function")try{await H.call(this,G);return!0}catch{}const W=this.insertText;if(typeof W!=="function")return!1;try{await W.call(this,G);return!0}catch{return!1}}async tryNativeKeyPress(G,H){const W=this.nativeKeyPress;if(typeof W!=="function")return!1;try{await W.call(this,G,H);return!0}catch{return!1}}async isResolvedFocused(){try{return await this.evaluate(`
|
|
48
48
|
(() => {
|
|
49
49
|
const el = window.__resolved;
|
|
50
50
|
return !!el && (document.activeElement === el || (typeof el.matches === 'function' && el.matches(':focus')));
|
|
@@ -77,6 +77,7 @@ export declare function resolveTargetJs(ref: string, opts?: ResolveOptions): str
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function boundingRectResolvedJs(opts?: {
|
|
79
79
|
skipScroll?: boolean;
|
|
80
|
+
forClick?: boolean;
|
|
80
81
|
}): string;
|
|
81
82
|
/**
|
|
82
83
|
* Generate JS for click that uses the unified resolver.
|
|
@@ -215,18 +215,112 @@ export function resolveTargetJs(j,g={}){const q=JSON.stringify(j),w=g.nth!==void
|
|
|
215
215
|
return { ok: true, matches_n: matches.length, match_level: 'exact' };
|
|
216
216
|
}
|
|
217
217
|
})()
|
|
218
|
-
`}export function boundingRectResolvedJs(j={}){return`
|
|
218
|
+
`}export function boundingRectResolvedJs(j={}){const g=j.skipScroll?"false":"true",q=j.forClick?"true":"false";return`
|
|
219
219
|
(() => {
|
|
220
220
|
const el = window.__resolved;
|
|
221
221
|
if (!el) throw new Error('No resolved element');
|
|
222
|
-
if (${
|
|
223
|
-
|
|
222
|
+
if (${g}) el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
223
|
+
|
|
224
|
+
const FOR_CLICK = ${q};
|
|
225
|
+
// hover()/dblClick() want the plain element centre — the retarget + hit-test
|
|
226
|
+
// below are click-only so those actions keep their original behaviour.
|
|
227
|
+
if (!FOR_CLICK) {
|
|
228
|
+
const r0 = el.getBoundingClientRect();
|
|
229
|
+
return {
|
|
230
|
+
x: Math.round(r0.left + r0.width / 2),
|
|
231
|
+
y: Math.round(r0.top + r0.height / 2),
|
|
232
|
+
w: Math.round(r0.width),
|
|
233
|
+
h: Math.round(r0.height),
|
|
234
|
+
visible: Math.round(r0.width) > 0 && Math.round(r0.height) > 0,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Does this node OWN a click handler? Deliberately excludes cursor:pointer,
|
|
239
|
+
// which is an *inherited* CSS property — an <svg> icon inside a clickable
|
|
240
|
+
// <div> inherits the pointer cursor but owns no handler, so cursor can't
|
|
241
|
+
// decide whether a node is the real click target. See issue #2071.
|
|
242
|
+
const ownsClickHandler = (node) => {
|
|
243
|
+
if (!node || node.nodeType !== 1) return false;
|
|
244
|
+
const tag = node.tagName.toLowerCase();
|
|
245
|
+
if (['a','button','input','select','textarea','label','summary'].includes(tag)) return true;
|
|
246
|
+
const role = node.getAttribute && node.getAttribute('role');
|
|
247
|
+
if (role && ['button','link','menuitem','menuitemcheckbox','menuitemradio','tab','option','checkbox','radio','switch'].includes(role)) return true;
|
|
248
|
+
if (typeof node.onclick === 'function') return true;
|
|
249
|
+
if (node.hasAttribute && (node.hasAttribute('onclick') || node.hasAttribute('jsaction'))) return true;
|
|
250
|
+
try {
|
|
251
|
+
for (const k in node) {
|
|
252
|
+
if (k.charCodeAt(0) === 95 && (k.indexOf('__reactProps$') === 0 || k.indexOf('__reactEventHandlers$') === 0)) {
|
|
253
|
+
const p = node[k];
|
|
254
|
+
if (p && (p.onClick || p.onMouseDown || p.onMouseUp)) return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {}
|
|
258
|
+
return false;
|
|
259
|
+
};
|
|
260
|
+
// A retarget destination just needs to look clickable; here cursor:pointer
|
|
261
|
+
// IS a useful signal (the ancestor is where it was set).
|
|
262
|
+
const isClickableAncestor = (node) => {
|
|
263
|
+
if (ownsClickHandler(node)) return true;
|
|
264
|
+
try { return window.getComputedStyle(node).cursor === 'pointer'; } catch (e) { return false; }
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// #2071: if the resolved node owns no click handler but a nearby ancestor
|
|
268
|
+
// is clickable, aim at that ancestor so the handler fires.
|
|
269
|
+
let target = el;
|
|
270
|
+
let retargeted = false;
|
|
271
|
+
if (!ownsClickHandler(el)) {
|
|
272
|
+
let a = el.parentElement, hops = 0;
|
|
273
|
+
while (a && hops < 4) {
|
|
274
|
+
if (isClickableAncestor(a)) { target = a; retargeted = true; break; }
|
|
275
|
+
a = a.parentElement; hops++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const rect = target.getBoundingClientRect();
|
|
224
280
|
const w = Math.round(rect.width);
|
|
225
281
|
const h = Math.round(rect.height);
|
|
226
|
-
|
|
227
|
-
|
|
282
|
+
let x = Math.round(rect.left + rect.width / 2);
|
|
283
|
+
let y = Math.round(rect.top + rect.height / 2);
|
|
228
284
|
const visible = w > 0 && h > 0;
|
|
229
|
-
|
|
285
|
+
|
|
286
|
+
// #2076: verify the click point actually lands on the target. A trusted
|
|
287
|
+
// CDP click hit-tests at (x,y) and delivers the event to whatever is
|
|
288
|
+
// topmost there — an overlay/sibling can silently swallow it. Classify:
|
|
289
|
+
// 'target' — the point is the target or a light-DOM descendant.
|
|
290
|
+
// 'ancestor' — the point is an ANCESTOR of the target. This is the
|
|
291
|
+
// open-shadow-DOM case (elementFromPoint returns the shadow
|
|
292
|
+
// *host*, not the shadow content) and the "point sits over
|
|
293
|
+
// the target's own wrapper background" case. In both a CDP
|
|
294
|
+
// click at (x,y) still reaches the target — CDP hit-testing
|
|
295
|
+
// pierces shadow roots, and light-DOM clicks bubble up — so
|
|
296
|
+
// it is trustworthy, unlike an unrelated overlay.
|
|
297
|
+
// 'other' — an unrelated element covers the point (the real overlay
|
|
298
|
+
// bug); the caller then dispatches a direct DOM click.
|
|
299
|
+
const hitClass = (px, py) => {
|
|
300
|
+
let at = null;
|
|
301
|
+
try { at = document.elementFromPoint(px, py); } catch (e) { return 'none'; }
|
|
302
|
+
if (!at) return 'none';
|
|
303
|
+
if (at === target || target.contains(at)) return 'target';
|
|
304
|
+
if (at.contains && at.contains(target)) return 'ancestor';
|
|
305
|
+
return 'other';
|
|
306
|
+
};
|
|
307
|
+
let hit = visible ? hitClass(x, y) : 'none';
|
|
308
|
+
if (visible && hit !== 'target' && hit !== 'ancestor') {
|
|
309
|
+
const cands = [
|
|
310
|
+
[rect.left + rect.width * 0.5, rect.top + rect.height * 0.25],
|
|
311
|
+
[rect.left + rect.width * 0.25, rect.top + rect.height * 0.5],
|
|
312
|
+
[rect.left + rect.width * 0.75, rect.top + rect.height * 0.5],
|
|
313
|
+
[rect.left + rect.width * 0.5, rect.top + rect.height * 0.75],
|
|
314
|
+
[rect.left + 3, rect.top + 3],
|
|
315
|
+
[rect.right - 3, rect.bottom - 3],
|
|
316
|
+
];
|
|
317
|
+
for (const c of cands) {
|
|
318
|
+
const px = Math.round(c[0]), py = Math.round(c[1]);
|
|
319
|
+
const hc = hitClass(px, py);
|
|
320
|
+
if (hc === 'target' || hc === 'ancestor') { x = px; y = py; hit = hc; break; }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { x, y, w, h, visible, hit, retargeted };
|
|
230
324
|
})()
|
|
231
325
|
`}export function clickResolvedJs(j={}){return`
|
|
232
326
|
(() => {
|