publishport-opencli 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-manifest.json +44 -2
- package/clis/chatgpt/auth.js +2 -2
- package/clis/chatgpt/image.js +1 -1
- package/clis/chatgpt/utils.js +1 -1
- package/clis/instagram/explore.js +37 -12
- package/clis/rednote/comments.js +1 -1
- package/clis/toutiao/hot.js +1 -1
- package/clis/toutiao/recommend.js +1 -0
- package/clis/toutiao/utils.js +2 -2
- package/clis/twitter/article.js +53 -12
- package/clis/twitter/profile.js +1 -1
- package/clis/xiaohongshu/comments.js +57 -14
- package/clis/xiaohongshu/human.js +14 -0
- package/clis/xiaohongshu/publish.js +35 -35
- package/clis/zhihu/paginate.js +2 -2
- package/dist/src/browser/cdp.js +1 -1
- package/dist/src/browser/managed-chrome.d.ts +5 -1
- package/dist/src/browser/managed-chrome.js +1 -1
- package/dist/src/cli.js +22 -22
- package/dist/src/execution.js +1 -1
- package/dist/src/plugin.js +1 -1
- package/dist/src/utils.d.ts +1 -1
- package/dist/src/utils.js +2 -5
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as _ from"node:fs";import*as D from"node:path";import{CommandExecutionError as V,ArgumentError as F}from"@jackwener/opencli/errors";import{cli as Yz,Strategy as Xz}from"@jackwener/opencli/registry";import{assertLiteralContent as Wz}from"../_shared/content-guard.js";import{humanType as A,humanWait as N,humanMoveTo as Fz,elementViewportCenter as Oz}from"./human.js";const Uz="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",u=9,d=20,n=3000,m="|||",k="基础",i=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],iz=i.map(([z])=>z),T="文字配图",l="再写一张",o="生成图片",s="下一步",S=".tiptap.ProseMirror",vz=["_onPublish","onPublish","_onSubmit","_handlePublish"],Bz=["_onSave","_onSaveDraft","_onDraft"],C=['[contenteditable="true"][placeholder*="标题"]','[contenteditable="true"][placeholder*="赞"]','input[placeholder*="标题"]','input[placeholder*="title" i]','[contenteditable="true"][class*="title"]','input[maxlength="20"]','input[class*="title"]',".title-input input",".note-title input","input[maxlength]"],r=['[contenteditable="true"][class*="content"]','[contenteditable="true"][class*="editor"]','[contenteditable="true"][placeholder*="描述"]','[contenteditable="true"][placeholder*="正文"]','[contenteditable="true"][placeholder*="内容"]','.note-content [contenteditable="true"]','.editor-content [contenteditable="true"]','[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])'],g={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp"};function W(z){if(z&&typeof z==="object"&&typeof z.session==="string"&&Object.prototype.hasOwnProperty.call(z,"data"))return z.data;return z}function Dz(z){return z.map((J)=>{const K=D.resolve(J);if(!_.existsSync(K))throw new F(`Image file not found: ${K}`);const Z=D.extname(K).toLowerCase();if(!g[Z])throw new F(`Unsupported image format "${Z}". Supported: jpg, png, gif, webp`);return K})}const p='input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]';async function fz(z,J=15000){const K=500,Z=Math.max(1,Math.ceil(J/K));for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
|
|
2
2
|
(() => {
|
|
3
|
-
const sels = ${JSON.stringify(
|
|
3
|
+
const sels = ${JSON.stringify(p)};
|
|
4
4
|
return !!document.querySelector(sels);
|
|
5
5
|
})()
|
|
6
|
-
`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function
|
|
6
|
+
`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function t(z,J){if(!await fz(z))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};if(z.setFileInput)try{await z.setFileInput(J,p);return{ok:!0,count:J.length}}catch($){const j=$ instanceof Error?$.message:String($);if(j.includes("Unknown action")||j.includes("not supported")||j.includes("Not allowed"));else return{ok:!1,count:0,error:j}}const Z=J.map(($)=>{const j=_.readFileSync($).toString("base64"),Y=D.extname($).toLowerCase();return{name:D.basename($),mimeType:g[Y],base64:j}}),Q=Z.reduce(($,j)=>$+j.base64.length,0);if(Q>500000)console.warn(`[warn] Total image payload is ${(Q/1024/1024).toFixed(1)}MB (base64). This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, or compress images before publishing.`);const q=JSON.stringify(Z);return z.evaluate(`
|
|
7
7
|
(async () => {
|
|
8
8
|
const images = ${q};
|
|
9
9
|
|
|
@@ -43,11 +43,11 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
43
43
|
|
|
44
44
|
return { ok: true, count: dt.files.length };
|
|
45
45
|
})()
|
|
46
|
-
`)}async function
|
|
46
|
+
`)}async function a(z,J=30000){const K=2000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(!await z.evaluate(`
|
|
47
47
|
() => !!document.querySelector(
|
|
48
48
|
'[class*="upload"][class*="progress"], [class*="uploading"], [class*="loading"][class*="image"]'
|
|
49
49
|
)
|
|
50
|
-
`))return;await z.wait({time:K/1000})}}async function
|
|
50
|
+
`))return;await z.wait({time:K/1000})}}async function e(z,J,K,Z){const Q=await z.evaluate(`
|
|
51
51
|
(function(selectors) {
|
|
52
52
|
const __opencli_xhs_fill_phase = "locate";
|
|
53
53
|
for (const sel of selectors) {
|
|
@@ -146,7 +146,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
146
146
|
fireBeforeInput(el, nextText);
|
|
147
147
|
return { ok: true };
|
|
148
148
|
})(${JSON.stringify(Q.sel)}, ${JSON.stringify(K)})
|
|
149
|
-
`))?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});throw Error(`Could not prepare ${Z} input. Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}try{await z
|
|
149
|
+
`))?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});throw Error(`Could not prepare ${Z} input. Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}try{await A(z,K);$=await z.evaluate(`
|
|
150
150
|
((selector, expectedText) => {
|
|
151
151
|
const __opencli_xhs_fill_phase = "verify";
|
|
152
152
|
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
@@ -169,7 +169,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
169
169
|
const actual = normalize(el.innerText || el.textContent || '');
|
|
170
170
|
return { ok: actual === normalize(expectedText), actual };
|
|
171
171
|
})(${JSON.stringify(Q.sel)}, ${JSON.stringify(K)})
|
|
172
|
-
`)}catch{$=await q()}}else $=await q();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});const j=typeof $?.actual==="string"?$.actual:"";throw Error(`Failed to set ${Z}. Expected "${K}", got "${j}". Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}}async function
|
|
172
|
+
`)}catch{$=await q()}}else $=await q();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});const j=typeof $?.actual==="string"?$.actual:"";throw Error(`Failed to set ${Z}. Expected "${K}", got "${j}". Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}}async function Nz(z,J){return W(await z.evaluate(`
|
|
173
173
|
(selectors => {
|
|
174
174
|
const el = selectors
|
|
175
175
|
.map(sel => Array.from(document.querySelectorAll(sel)))
|
|
@@ -185,7 +185,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
185
185
|
selection?.addRange(range);
|
|
186
186
|
return true;
|
|
187
187
|
})(${JSON.stringify(J)})
|
|
188
|
-
`))}function
|
|
188
|
+
`))}function lz(z,{click:J=!1}={}){return`
|
|
189
189
|
(topicName => {
|
|
190
190
|
const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
|
|
191
191
|
const want = norm(topicName);
|
|
@@ -225,7 +225,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
225
225
|
text: (target.innerText || target.textContent || '').trim().slice(0, 40),
|
|
226
226
|
};
|
|
227
227
|
})(${JSON.stringify(z)})
|
|
228
|
-
`}function
|
|
228
|
+
`}function oz(z,J){return`
|
|
229
229
|
((topicName, selectors) => {
|
|
230
230
|
const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
|
|
231
231
|
const want = norm(topicName);
|
|
@@ -256,7 +256,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
256
256
|
}
|
|
257
257
|
return count;
|
|
258
258
|
})(${JSON.stringify(z)}, ${JSON.stringify(J)})
|
|
259
|
-
`}function
|
|
259
|
+
`}function zz(z,J){return`
|
|
260
260
|
((topicName, selectors) => {
|
|
261
261
|
const __opencli_xhs_topic_marker_count = true;
|
|
262
262
|
const marker = '#' + topicName + '[话题]';
|
|
@@ -274,14 +274,14 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
274
274
|
}
|
|
275
275
|
return count;
|
|
276
276
|
})(${JSON.stringify(z)}, ${JSON.stringify(J)})
|
|
277
|
-
`}async function
|
|
277
|
+
`}async function sz(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return W(await z.evaluate(`
|
|
278
278
|
(text => {
|
|
279
279
|
const ok = document.execCommand('insertText', false, text);
|
|
280
280
|
const active = document.activeElement;
|
|
281
281
|
if (active) active.dispatchEvent(new Event('input', { bubbles: true }));
|
|
282
282
|
return ok;
|
|
283
283
|
})(${JSON.stringify(K)})
|
|
284
|
-
`))}async function
|
|
284
|
+
`))}async function kz(z,J,K){const Z=[];for(const Q of K){if(!await Nz(z,J))throw new V(`Could not attach topic "${Q}": body editor not found`);const $=Number(W(await z.evaluate(zz(Q,J))))||0;if(typeof z.pressKey==="function")try{await z.pressKey("Enter")}catch{}if(typeof z.insertText!=="function")throw new V(`Could not attach topic "${Q}": page.insertText is unavailable`);try{await A(z,`#${Q}`)}catch{throw new V(`Could not attach topic "${Q}": failed to type inline topic query`)}await N(z,1.2);if(typeof z.pressKey!=="function")throw new V(`Could not attach topic "${Q}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch(Y){throw new V(`Could not attach topic "${Q}": failed to accept suggestion (${Y&&Y.message||Y})`)}await z.wait({time:0.8});if((Number(W(await z.evaluate(zz(Q,J))))||0)<=$)throw new V(`Could not attach topic "${Q}": no real topic entity appeared after selection`);Z.push(Q);await z.wait({time:0.4})}return Z}async function Lz(z){const J=await z.evaluate(`
|
|
285
285
|
() => {
|
|
286
286
|
const isVisible = (el) => {
|
|
287
287
|
if (!el || el.offsetParent === null) return false;
|
|
@@ -330,7 +330,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
330
330
|
}
|
|
331
331
|
return { ok: false, visibleTexts };
|
|
332
332
|
}
|
|
333
|
-
`);if(J?.ok)await z.wait({time:1});return J}async function
|
|
333
|
+
`);if(J?.ok)await z.wait({time:1});return J}async function L(z,J,K=7000){const Z=500,Q=Math.max(1,Math.ceil(K/Z));let q={ok:!1};for(let $=0;$<Q;$++){q=W(await z.evaluate(`
|
|
334
334
|
((cfg) => {
|
|
335
335
|
const __opencli_xhs_click_label = { wantLabel: ${JSON.stringify(J)} };
|
|
336
336
|
const wantLabel = ${JSON.stringify(J)};
|
|
@@ -365,10 +365,10 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
365
365
|
}
|
|
366
366
|
return { ok: false };
|
|
367
367
|
})()
|
|
368
|
-
`));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function
|
|
368
|
+
`));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function Pz(z){const J=await z.evaluate(`
|
|
369
369
|
(() => {
|
|
370
370
|
const __opencli_xhs_focus_card = true;
|
|
371
|
-
const sel = ${JSON.stringify(
|
|
371
|
+
const sel = ${JSON.stringify(S)};
|
|
372
372
|
const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
|
|
373
373
|
// Prefer the editor inside the active swiper slide if present.
|
|
374
374
|
const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
|
|
@@ -382,34 +382,34 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
382
382
|
selection?.addRange(range);
|
|
383
383
|
return { ok: true };
|
|
384
384
|
})()
|
|
385
|
-
`);return W(J)}async function
|
|
385
|
+
`);return W(J)}async function Iz(z){const J=await z.evaluate(`
|
|
386
386
|
(() => {
|
|
387
387
|
const __opencli_xhs_card_text = true;
|
|
388
|
-
const sel = ${JSON.stringify(
|
|
388
|
+
const sel = ${JSON.stringify(S)};
|
|
389
389
|
const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
|
|
390
390
|
const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
|
|
391
391
|
const text = active ? (active.innerText || active.textContent || '').trim() : '';
|
|
392
392
|
return { ok: !!text, text };
|
|
393
393
|
})()
|
|
394
|
-
`);return W(J)}async function
|
|
394
|
+
`);return W(J)}async function Jz(z){const J=await z.evaluate(`
|
|
395
395
|
(() => {
|
|
396
396
|
const __opencli_xhs_card_count = true;
|
|
397
|
-
const sel = ${JSON.stringify(
|
|
397
|
+
const sel = ${JSON.stringify(S)};
|
|
398
398
|
const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
|
|
399
399
|
const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
|
|
400
400
|
const activeText = active ? (active.innerText || active.textContent || '').trim() : '';
|
|
401
401
|
return { ok: true, count: editors.length, activeEmpty: !activeText };
|
|
402
402
|
})()
|
|
403
|
-
`);return W(J)}async function
|
|
403
|
+
`);return W(J)}async function hz(z,J=8000){const K=300,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if((await Jz(z))?.count>=1)return!0;await z.wait({time:K/1000})}return!1}async function Mz(z,J,K=6000){const Z=300,Q=Math.ceil(K/Z);for(let q=0;q<Q;q++){const $=await Jz(z);if($?.count>=J&&$?.activeEmpty)return!0;await z.wait({time:Z/1000})}return!1}async function wz(z,J,K=4){for(let Z=0;Z<K;Z++){if(!(await L(z,l))?.ok)return!1;if(await Mz(z,J,2500))return!0}return!1}async function Rz(z){const J=await z.evaluate(`
|
|
404
404
|
(() => {
|
|
405
405
|
const __opencli_xhs_preview_ready = true;
|
|
406
406
|
const ready = Array.from(document.querySelectorAll('button'))
|
|
407
407
|
.some((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '下一步');
|
|
408
408
|
return { ok: ready };
|
|
409
409
|
})()
|
|
410
|
-
`);return W(J)}async function
|
|
411
|
-
`);if(!(await
|
|
412
|
-
`);for(let $=0;$<q.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(q[$])await z
|
|
410
|
+
`);return W(J)}async function _z(z,J=6){const K=400,Z=Math.ceil(3000/K);for(let Q=0;Q<J;Q++){if(!(await L(z,o))?.ok&&Q===0)return!1;for(let $=0;$<Z;$++){if((await Rz(z))?.ok)return!0;await z.wait({time:K/1000})}}return!1}async function bz(z,J,K){J=String(J).replace(/\\n/g,`
|
|
411
|
+
`);if(!(await Pz(z))?.ok)throw new V(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const q=J.split(`
|
|
412
|
+
`);for(let $=0;$<q.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(q[$])await A(z,q[$])}}else await z.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(J)})`);await z.wait({time:0.4});if(!(await Iz(z))?.ok)throw new V(`文字配图: card editor #${K+1} is empty after typing`)}async function yz(z,J){if(!J||J===k)return k;const K=W(await z.evaluate(`
|
|
413
413
|
(async () => {
|
|
414
414
|
const __opencli_xhs_card_styles = true;
|
|
415
415
|
const want = ${JSON.stringify(J)};
|
|
@@ -438,7 +438,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
438
438
|
}
|
|
439
439
|
return { ok: seen.length > 0, styles: seen, found: false };
|
|
440
440
|
})()
|
|
441
|
-
`));if(!K?.found)throw new
|
|
441
|
+
`));if(!K?.found)throw new V(`文字配图: requested style "${J}" is not available for this content (options: ${(K?.styles||[]).join(" / ")||"none"}). Choose an available style or omit --card-style to use ${k}.`);if(!(await L(z,J))?.ok)throw new V(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function Kz(z){const J=await z.evaluate(`
|
|
442
442
|
(() => {
|
|
443
443
|
const __opencli_xhs_composer_media_count = true;
|
|
444
444
|
const visibleBox = (el) => {
|
|
@@ -451,7 +451,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
451
451
|
const r = el.getBoundingClientRect();
|
|
452
452
|
return r.width >= 48 && r.height >= 48;
|
|
453
453
|
};
|
|
454
|
-
const titleSelectors = ${JSON.stringify(
|
|
454
|
+
const titleSelectors = ${JSON.stringify(C)};
|
|
455
455
|
const titleEl = titleSelectors
|
|
456
456
|
.map((sel) => Array.from(document.querySelectorAll(sel)))
|
|
457
457
|
.flat()
|
|
@@ -470,7 +470,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
470
470
|
}
|
|
471
471
|
return { ok: true, count };
|
|
472
472
|
})()
|
|
473
|
-
`);return W(J)}async function
|
|
473
|
+
`);return W(J)}async function Qz(z,J,K){const Z=await Kz(z);if(!Z||typeof Z.count!=="number")throw new V(`${K}: could not verify current composer media count`);if(Z.count<J){await z.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new V(`${K}: expected at least ${J} visible media item(s), got ${Z.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function Az(z,J,K){if(!(await L(z,T))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new V(`文字配图: could not click "${T}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await hz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new V(`文字配图: 写文字 card editor did not appear after clicking "${T}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let j=0;j<J.length;j++){if(j>0){if(!await wz(z,j+1)){await z.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new V(`文字配图: new card editor #${j+1} did not render after "${l}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await bz(z,J[j],j)}if(!await _z(z)){await z.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new V(`文字配图: "${o}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const q=await yz(z,K);if(!(await L(z,s))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new V(`文字配图: could not click "${s}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return q}async function Zz(z){return z.evaluate(`
|
|
474
474
|
() => {
|
|
475
475
|
const text = (document.body?.innerText || '').replace(/s+/g, ' ').trim();
|
|
476
476
|
const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
|
|
@@ -500,22 +500,22 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
500
500
|
const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
|
|
501
501
|
return { state, hasTitleInput, hasImageInput, hasVideoSurface };
|
|
502
502
|
}
|
|
503
|
-
`)}async function
|
|
503
|
+
`)}async function Tz(z,J=5000){const K=500,Z=Math.max(1,Math.ceil(J/K));let Q=await Zz(z);for(let q=0;q<Z;q++){if(Q.state!=="video_surface")return Q;if(q<Z-1){await z.wait({time:K/1000});Q=await Zz(z)}}return Q}async function Sz(z,J=1e4){const K=1000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
|
|
504
504
|
(() => {
|
|
505
|
-
const sels = ${JSON.stringify(
|
|
505
|
+
const sels = ${JSON.stringify(C)};
|
|
506
506
|
for (const sel of sels) {
|
|
507
507
|
const el = document.querySelector(sel);
|
|
508
508
|
if (el && el.offsetParent !== null) return true;
|
|
509
509
|
}
|
|
510
510
|
return false;
|
|
511
|
-
})()`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function
|
|
511
|
+
})()`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function Cz(z){const J=W(await z.evaluate(`
|
|
512
512
|
(() => {
|
|
513
513
|
const __opencli_xhs_restore_srcs = true;
|
|
514
514
|
return Array.from(document.querySelectorAll('.img-container img'))
|
|
515
515
|
.map((img) => img.currentSrc || img.src || '')
|
|
516
516
|
.filter(Boolean);
|
|
517
517
|
})()
|
|
518
|
-
`));return Array.isArray(J)?J:[]}async function
|
|
518
|
+
`));return Array.isArray(J)?J:[]}async function $z(z,J){if(!Array.isArray(J)||J.length===0)return 0;const K=W(await z.evaluate(`
|
|
519
519
|
(async (wanted) => {
|
|
520
520
|
const __opencli_xhs_delete_by_src = true;
|
|
521
521
|
const want = new Set(wanted);
|
|
@@ -546,7 +546,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
546
546
|
}
|
|
547
547
|
return removed;
|
|
548
548
|
})(${JSON.stringify(J)})
|
|
549
|
-
`));return Number(K)||0}function
|
|
549
|
+
`));return Number(K)||0}function xz(z){const J=String(z&&(z.url||z.key)||""),K=String(z&&z.method||"").toUpperCase(),Z=Number(z&&z.status||0);if(K!=="PUT")return null;if(!/ros-upload[^/]*\/spectrum\//.test(J))return null;if(Z<200||Z>=300)return null;const Q=J.match(/\/spectrum\/([^?#]+)/);return Q?Q[1]:J}function qz(z){return Math.min(45,6+z*4)}async function jz(z,J,K,Z=90000){if(J<=0)return{ok:!0,completed:0};if(!K||typeof z.readNetworkCapture!=="function"){await z.wait({time:qz(J)});return{ok:!0,completed:null,viaFallback:!0}}const Q=1500,$=Math.ceil(9000/Q),j=Math.ceil(Z/Q),Y=new Set;for(let U=0;U<j;U++){const f=await z.readNetworkCapture().catch(()=>[]);for(const v of Array.isArray(f)?f:[]){const G=xz(v);if(G)Y.add(G)}if(Y.size>=J)return{ok:!0,completed:Y.size};if(U>=$&&Y.size===0){await z.wait({time:qz(J)});return{ok:!0,completed:null,viaFallback:!0}}await z.wait({time:Q/1000})}return{ok:!1,completed:Y.size}}Yz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Xz.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${m} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${k}。可选: ${i.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Z=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(Z){if(Q)throw new F("正文位置参数和 --file 只能二选一,不能同时给");const H=D.resolve(Z);if(!_.existsSync(H))throw new F(`--file 指向的文件不存在: ${H}`);Q=_.readFileSync(H,"utf-8").trim();if(!Q)throw new F(`--file 指向的文件内容为空: ${H}`)}const q=J.images?String(J.images).split(",").map((H)=>H.trim()).filter(Boolean):[],$=J.topics?String(J.topics).split(",").map((H)=>H.trim()).filter(Boolean):[],j=Boolean(J.draft),Y=J["card-text"]?String(J["card-text"]):"",U=Y?Y.split(m).map((H)=>H.trim()).filter(Boolean):[],f=J["card-style"]?String(J["card-style"]).trim():"",v=U.length>0;if(!K)throw new F("--title is required");if(K.length>d)throw new F(`Title is ${K.length} chars — must be ≤ ${d}`);if(!Q)throw new F("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");Wz(Q,{fileFlag:"--file"});if(!v&&q.length===0)throw new F("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>u)throw new F(`Too many images: ${q.length} (max ${u})`);if(v&&q.some((H)=>D.extname(H).toLowerCase()===".gif"))throw new F("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const G=Dz(q);await z.goto(Uz);await z.wait({time:1});await z.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let P="";for(let H=0;H<30;H++){await z.wait({time:0.5});P=await z.evaluate("() => location.href");if(P.includes("creator.xiaohongshu.com"))break}if(!P.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${P}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const I=await Lz(z);if((await Tz(z,I?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const H=I?.ok?`clicked "${I.text}"`:`visible candidates: ${(I?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${H}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const h=await Cz(z);let M=f;if(v)M=await Az(z,U,f);else{const H=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await t(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new V(`图片注入失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await z.wait({time:n/1000});const B=await jz(z,G.length,H);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new V(`图片上传未全部完成:只确认 ${B.completed}/${G.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await a(z);if(h.length){await $z(z,h);await z.wait({time:0.5})}const R=await Kz(z);if(R&&typeof R.count==="number"&&R.count>G.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new V(`清掉恢复态旧图后编辑器内仍有 ${R.count} 张图,超过本次注入的 ${G.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await Sz(z)){await z.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new V("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(v)await Qz(z,U.length,"文字配图 generated images");if(v&&G.length>0){const H=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await t(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new V(`追加图片失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await z.wait({time:n/1000});const B=await jz(z,G.length,H);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new V(`追加图片上传未全部完成:只确认 ${B.completed}/${G.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await a(z);await Qz(z,U.length+G.length,"文字配图 appended images")}if(v&&h.length){await $z(z,h);await z.wait({time:0.5})}await e(z,C,K,"title");await N(z,0.6);await e(z,r,Q,"content");await N(z,0.6);let b=[];if($.length)b=await kz(z,r,$);await N(z,1,0.6);const y=await Oz(z,["xhs-publish-btn"]);if(y){await Fz(z,y.x,y.y);await N(z,0.4)}const x=j?["暂存离开","存草稿"]:["发布","发布笔记"],X=await z.evaluate(`
|
|
550
550
|
(cfg => {
|
|
551
551
|
const { isDraftMode, publishNames, draftNames, labels } = cfg;
|
|
552
552
|
const isVisible = (el) => {
|
|
@@ -585,7 +585,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
585
585
|
}
|
|
586
586
|
}
|
|
587
587
|
return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
|
|
588
|
-
})(${JSON.stringify({isDraftMode:j,publishNames:
|
|
588
|
+
})(${JSON.stringify({isDraftMode:j,publishNames:vz,draftNames:Bz,labels:x})})
|
|
589
589
|
`);if(!X?.ok){if(j){if(await z.evaluate(`
|
|
590
590
|
(() => {
|
|
591
591
|
const labels = ['返回', '关闭', '取消', '离开'];
|
|
@@ -624,7 +624,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
624
624
|
}
|
|
625
625
|
return false;
|
|
626
626
|
}
|
|
627
|
-
`)){X.ok=!0;X.via="auto-save"}}}}if(!X?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const
|
|
627
|
+
`)){X.ok=!0;X.via="auto-save"}}}}if(!X?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const H=X?.via?` (via=${X.via})`:"",O=X?.error?`, error=${X.error}`:"",B=X?.lastMethodError?`, lastMethodError=${X.lastMethodError}`:"";throw Error(`未能触发「${x[0]}」按钮${H}${O}${B}。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_submit_debug.png")}await z.wait({time:4});const w=await z.evaluate("() => location.href"),Hz=j?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],E=await z.evaluate(`
|
|
628
628
|
(markers => {
|
|
629
629
|
for (const el of document.querySelectorAll('*')) {
|
|
630
630
|
if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
|
|
@@ -633,5 +633,5 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
633
633
|
if (el.children.length === 0 && markers.some(marker => text.includes(marker))) return text;
|
|
634
634
|
}
|
|
635
635
|
return '';
|
|
636
|
-
})(${JSON.stringify(
|
|
637
|
-
`),
|
|
636
|
+
})(${JSON.stringify(Hz)})
|
|
637
|
+
`),Vz=!w.includes("/publish/publish"),Gz=E.length>0||Vz,c=j?"暂存成功":"发布成功";if(!Gz)throw new V(`${c}无法确认:未观察到成功提示或提交后跳转。`+(w?`当前 URL: ${w}。`:"当前 URL 为空。")+"若确属失败,直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。");return[{status:`✅ ${c}`,detail:[`"${K}"`,v?`${U.length}张文字配图${G.length?` + ${G.length}张图片`:""}${M&&M!==k?` (${M})`:""}`:`${G.length}张图片`,b.length?`话题: ${b.join(" ")}`:"",E||w||""].filter(Boolean).join(" · ")}]}});
|
package/clis/zhihu/paginate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ArgumentError as V,AuthRequiredError as F,CommandExecutionError as K,EmptyResultError as W}from"@jackwener/opencli/errors";export const MAX_LIMIT=1000;export function validateLimit(G,B=20){const J=G??B;let D;if(typeof J==="number")D=J;else{const H=String(J).trim();if(!/^\d+$/.test(H))throw new V(`--limit must be a positive integer no greater than ${MAX_LIMIT}`,"Use a normal-sized decimal integer limit to avoid slow requests or Zhihu risk controls");D=Number(H)}if(!Number.isInteger(D)||D<=0||D>MAX_LIMIT)throw new V(`--limit must be a positive integer no greater than ${MAX_LIMIT}`,"Use a normal-sized decimal integer limit to avoid slow requests or Zhihu risk controls");return D}export function unwrapEvaluateResult(G){if(G&&typeof G==="object"&&"data"in G&&"session"in G)return G.data;return G}export function requireZhihuListPayload(G,B,J){const D=unwrapEvaluateResult(G);if(!D||typeof D!=="object"||Array.isArray(D))throw new K(`Zhihu ${B} returned malformed payload`,`URL: ${J}`);if(D.__httpError){const H=D.__httpError;if(H===401||H===403)throw new F("www.zhihu.com",`Failed to fetch Zhihu ${B}`);if(H===404)throw new W(`zhihu ${B}`,"Check the target identifier");throw new K(`Zhihu ${B} request failed${H?` (HTTP ${H})`:""}`,"Try again later or rerun with -v")}if(D.__fetchError)throw new K(`Zhihu ${B} request failed`,String(D.__fetchError));if(!Array.isArray(D.data))throw new K(`Zhihu ${B} returned malformed data list`,`URL: ${J}`);if(!D.paging||typeof D.paging!=="object")throw new K(`Zhihu ${B} returned malformed paging data`,`URL: ${J}`);return D}function Y(G){if(typeof G!=="string"||!G)return"";try{const B=new URL(G);if(B.protocol!=="https:")return"";if(B.hostname==="api.zhihu.com"&&B.pathname.startsWith("/members/"))return`https://www.zhihu.com/api/v4${B.pathname}${B.search}`;if(B.hostname==="www.zhihu.com"&&B.pathname.startsWith("/api/v4/members/"))return B.toString()}catch{return""}return""}function $(G,B){try{return new URL(G).pathname===new URL(B).pathname}catch{return!1}}export async function fetchZhihuList(G,B,J,D){const H=[],Q=new Set;let N=B;while(N&&H.length<J&&!Q.has(N)){Q.add(N);const S=requireZhihuListPayload(await G.evaluate(`
|
|
1
|
+
import{ArgumentError as V,AuthRequiredError as F,CommandExecutionError as K,EmptyResultError as W}from"@jackwener/opencli/errors";export const MAX_LIMIT=1000;export function validateLimit(G,B=20){const J=G??B;let D;if(typeof J==="number")D=J;else{const H=String(J).trim();if(!/^\d+$/.test(H))throw new V(`--limit must be a positive integer no greater than ${MAX_LIMIT}`,"Use a normal-sized decimal integer limit to avoid slow requests or Zhihu risk controls");D=Number(H)}if(!Number.isInteger(D)||D<=0||D>MAX_LIMIT)throw new V(`--limit must be a positive integer no greater than ${MAX_LIMIT}`,"Use a normal-sized decimal integer limit to avoid slow requests or Zhihu risk controls");return D}export function unwrapEvaluateResult(G){if(G&&typeof G==="object"&&"data"in G&&"session"in G)return G.data;return G}export function requireZhihuListPayload(G,B,J){const D=unwrapEvaluateResult(G);if(!D||typeof D!=="object"||Array.isArray(D))throw new K(`Zhihu ${B} returned malformed payload`,`URL: ${J}`);if(D.__httpError){const H=D.__httpError;if(H===401||H===403)throw new F("www.zhihu.com",`Failed to fetch Zhihu ${B}`);if(H===404)throw new W(`zhihu ${B}`,"Check the target identifier");throw new K(`Zhihu ${B} request failed${H?` (HTTP ${H})`:""}`,"Try again later or rerun with -v")}if(D.__fetchError)throw new K(`Zhihu ${B} request failed`,String(D.__fetchError));if(!Array.isArray(D.data))throw new K(`Zhihu ${B} returned malformed data list`,`URL: ${J}`);if(!D.paging||typeof D.paging!=="object")throw new K(`Zhihu ${B} returned malformed paging data`,`URL: ${J}`);return D}function Y(G){if(typeof G!=="string"||!G)return"";try{const B=new URL(G);if(B.protocol!=="https:"&&B.protocol!=="http:")return"";B.protocol="https:";if(B.hostname==="api.zhihu.com"&&B.pathname.startsWith("/members/"))return`https://www.zhihu.com/api/v4${B.pathname}${B.search}`;if(B.hostname==="www.zhihu.com"&&B.pathname.startsWith("/api/v4/members/"))return B.toString()}catch{return""}return""}function $(G,B){try{return new URL(G).pathname===new URL(B).pathname}catch{return!1}}export async function fetchZhihuList(G,B,J,D){const H=[],Q=new Set;let N=B;while(N&&H.length<J&&!Q.has(N)){Q.add(N);const S=requireZhihuListPayload(await G.evaluate(`
|
|
2
2
|
(async () => {
|
|
3
3
|
try {
|
|
4
4
|
const r = await fetch(${JSON.stringify(N)}, { credentials: 'include' });
|
|
@@ -8,4 +8,4 @@ import{ArgumentError as V,AuthRequiredError as F,CommandExecutionError as K,Empt
|
|
|
8
8
|
return { __fetchError: err?.message || String(err) };
|
|
9
9
|
}
|
|
10
10
|
})()
|
|
11
|
-
`),D,N);for(const j of S.data){H.push(j);if(H.length>=J)break}if(S.paging?.is_end)break;const O=Y(S.paging?.next);if(!O||!$(O,B))throw new K(`Zhihu ${D} pagination returned malformed next URL`);if(Q.has(O))throw new K(`Zhihu ${D} pagination returned a repeated next URL`);N=O}if(H.length===0)throw new W(`zhihu ${D}`,"No rows were returned for the requested Zhihu user.");return H}export const __test__={normalizeZhihuApiUrl:Y,sameZhihuApiPath:$};
|
|
11
|
+
`),D,N);for(const j of S.data){H.push(j);if(H.length>=J)break}if(H.length>=J)break;if(S.paging?.is_end)break;const O=Y(S.paging?.next);if(!O||!$(O,B))throw new K(`Zhihu ${D} pagination returned malformed next URL`);if(Q.has(O))throw new K(`Zhihu ${D} pagination returned a repeated next URL`);N=O}if(H.length===0)throw new W(`zhihu ${D}`,"No rows were returned for the requested Zhihu user.");return H}export const __test__={normalizeZhihuApiUrl:Y,sameZhihuApiPath:$};
|
package/dist/src/browser/cdp.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(L)=>{if(L?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.proxyAuth){this.on("Fetch.requestPaused",(L)=>{const Y=L;if(Y.requestId)this.send("Fetch.continueRequest",{requestId:Y.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(L)=>{const Y=L;if(!Y.requestId)return;this.send("Fetch.continueWithAuth",{requestId:Y.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(L){Z.close();X(L instanceof Error?L:Error(String(L)));return}K(new O(this))});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);const J=G.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `ppcli browser dialog accept`;托管 profile 可 `ppcli profile stop <name>` 关闭后重跑命令自动重启会话)":"";Z(Error(`CDP command '${G}' timed out after ${Q/1000}s${J}`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
|
|
1
|
+
import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(Y)=>{if(Y?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.windowMode==="background")await this.send("Emulation.setFocusEmulationEnabled",{enabled:!0}).catch(()=>{});else if(G?.windowMode==="foreground")await this.send("Page.bringToFront").catch(()=>{});if(G?.proxyAuth){this.on("Fetch.requestPaused",(Y)=>{const A=Y;if(A.requestId)this.send("Fetch.continueRequest",{requestId:A.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(Y)=>{const A=Y;if(!A.requestId)return;this.send("Fetch.continueWithAuth",{requestId:A.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(Y){Z.close();X(Y instanceof Error?Y:Error(String(Y)));return}const L=new O(this);if(G?.session)L.session=G.session;if(G?.contextId)L.contextId=G.contextId;K(L)});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);const J=G.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `ppcli browser dialog accept`;托管 profile 可 `ppcli profile stop <name>` 关闭后重跑命令自动重启会话)":"";Z(Error(`CDP command '${G}' timed out after ${Q/1000}s${J}`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;session;contextId;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
|
|
@@ -19,8 +19,12 @@ export declare function isManagedChromeRunning(entry: ManagedProfileEntry): Prom
|
|
|
19
19
|
* 已在跑 → 直接复用;没在跑 → 拉起并等就绪。
|
|
20
20
|
* 「探测→拉起→等就绪」整段按 profile 互斥(进程内 Promise 去重 + 跨进程文件锁),
|
|
21
21
|
* 保证任一时刻同一 profile 只有一路真正执行启动。
|
|
22
|
+
* windowMode 决定窗口状态协调:background 新建窗口即最小化(真后台),
|
|
23
|
+
* foreground 把最小化窗口还原(登录等用户必须看见的流程)。
|
|
22
24
|
*/
|
|
23
|
-
export declare function ensureManagedChrome(name: string, entry: ManagedProfileEntry
|
|
25
|
+
export declare function ensureManagedChrome(name: string, entry: ManagedProfileEntry, opts?: {
|
|
26
|
+
windowMode?: 'foreground' | 'background';
|
|
27
|
+
}): Promise<string>;
|
|
24
28
|
/** 通过 CDP Browser.close 优雅关闭托管 Chrome。返回是否真的关了(本来没跑返回 false)。 */
|
|
25
29
|
export declare function stopManagedChrome(entry: ManagedProfileEntry): Promise<boolean>;
|
|
26
30
|
export type ManagedProfileOpts = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execFileSync as R}from"node:child_process";import*as B from"node:fs";import*as W from"node:path";import{request as x}from"node:http";import{WebSocket as T}from"ws";import{probeCDP as q,launchDetachedApp as v}from"../launcher.js";import{withBrowserSessionLock as C}from"../browser-session-lock.js";import{CommandExecutionError as O}from"../errors.js";import{log as J}from"../logger.js";import{addManagedProfile as M,getManagedProfile as N,listManagedProfiles as _,managedProfilesBaseDir as w,removeManagedProfileEntry as b,updateManagedProfile as A}from"./profile.js";import{proxyServerFlag as D}from"./proxy-test.js";import{resolveFingerprint as E,randomSeed as P}from"./fingerprint.js";const f=500,L=20000,j=9301,k=9399;function F(K){for(const H of K)try{B.accessSync(H,B.constants.X_OK);return H}catch{}return null}function g(K){for(const H of K)try{const Q=R("which",[H],{encoding:"utf-8",stdio:"pipe"}).trim();if(Q)return Q}catch{}return null}export function discoverChromePath(){const K=process.env.OPENCLI_CHROME_PATH?.trim();if(K)return K;if(process.platform==="darwin")return F(["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]);if(process.platform==="win32"){const H=process.env.ProgramFiles??"C:\\Program Files",Q=process.env["ProgramFiles(x86)"]??"C:\\Program Files (x86)",Y=process.env.LOCALAPPDATA??"";return F([W.join(H,"Google","Chrome","Application","chrome.exe"),W.join(Q,"Google","Chrome","Application","chrome.exe"),...Y?[W.join(Y,"Google","Chrome","Application","chrome.exe")]:[],W.join(H,"Microsoft","Edge","Application","msedge.exe"),W.join(Q,"Microsoft","Edge","Application","msedge.exe")])}return g(["google-chrome-stable","google-chrome","chromium","chromium-browser","microsoft-edge-stable"])}function G(K,H,Q=5000){return new Promise((Y,Z)=>{const V=new URL(H),$=x({hostname:V.hostname,port:V.port,path:V.pathname+V.search,method:K,timeout:Q},(z)=>{const U=[];z.on("data",(X)=>U.push(Buffer.isBuffer(X)?X:Buffer.from(X)));z.on("end",()=>{const X=Buffer.concat(U).toString("utf8");if((z.statusCode??0)<200||(z.statusCode??0)>=300){Z(Error(`HTTP ${z.statusCode}: ${X.slice(0,200)}`));return}try{Y(X?JSON.parse(X):null)}catch{Y(X)}})});$.on("error",Z);$.on("timeout",()=>$.destroy(Error(`请求 ${H} 超时`)));$.end()})}export function managedEndpoint(K){return`http://127.0.0.1:${K.port}`}export async function isManagedChromeRunning(K){return q(K.port)}const m=["--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-domain-reliability","--disable-breakpad","--disable-client-side-phishing-detection","--no-pings","--no-service-autorun","--password-store=basic","--disable-features=Translate,OptimizationHints,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions,CalculateNativeWinOcclusion"];function d(K){const H=[`--user-data-dir=${K.userDataDir}`,`--remote-debugging-port=${K.port}`,"--remote-allow-origins=*"];if(K.clean!==!1)H.push(...m);if(K.webrtc!==!1)H.push("--force-webrtc-ip-handling-policy=disable_non_proxied_udp");if(K.proxy)H.push(`--proxy-server=${D(K.proxy)}`);if(K.fingerprint?.enabled&&K.fingerprint.locale)H.push(`--lang=${K.fingerprint.locale}`);H.push("about:blank");return H}function u(K){const H=K.fingerprint?.enabled?K.fingerprint.timezone:void 0;return H?{TZ:H}:void 0}async function y(K,H){const Q=Date.now()+L;while(Date.now()<Q){if(await q(K,1000))return;await new Promise((Y)=>setTimeout(Y,f))}throw new O(`托管 Chrome「${H}」已启动但 ${L/1000}s 内 CDP 端口 ${K} 未就绪`,"浏览器可能启动缓慢,稍后重试;或检查该端口是否被其它进程占用。")}async function S(K){try{const H=await G("GET",`${K}/json`);if(!(Array.isArray(H)&&H.some((Y)=>Y.type==="page")))await G("PUT",`${K}/json/new?about:blank`)}catch(H){J.debug(`[managed-chrome] ensurePageTarget 失败(非致命):${H instanceof Error?H.message:H}`)}}const I=new Map;export async function ensureManagedChrome(K,H){const Q=managedEndpoint(H),Y=I.get(K);if(Y)return Y;if(await q(H.port)){J.debug(`[managed-chrome] profile「${K}」已在端口 ${H.port} 运行`);await S(Q);return Q}const Z=I.get(K);if(Z)return Z;const V=C(async()=>{if(await q(H.port)){J.debug(`[managed-chrome] profile「${K}」等锁期间已被拉起,直接复用`);await S(Q);return Q}const $=H.browserPath??discoverChromePath();if(!$)throw new O("找不到可用的 Chrome/Chromium 浏览器。","安装 Google Chrome,或用 OPENCLI_CHROME_PATH 环境变量指定可执行文件路径。");B.mkdirSync(H.userDataDir,{recursive:!0});J.debug(`[managed-chrome] 拉起 profile「${K}」:${$} 端口 ${H.port}${H.proxy?" (代理)":""}`);await v($,d(H),`托管 profile ${K}`,u(H));await y(H.port,K);await S(Q);return Q},`launch-${K}`).finally(()=>I.delete(K));I.set(K,V);return V}export async function stopManagedChrome(K){if(!await q(K.port))return!1;const Q=(await G("GET",`${managedEndpoint(K)}/json/version`))?.webSocketDebuggerUrl;if(!Q)throw Error(`端口 ${K.port} 上的 CDP 端点没有暴露 browser target`);await new Promise((Z,V)=>{const $=new T(Q),z=setTimeout(()=>{$.close();V(Error("CDP Browser.close 超时"))},1e4);$.on("open",()=>{$.send(JSON.stringify({id:1,method:"Browser.close",params:{}}))});$.on("close",()=>{clearTimeout(z);Z()});$.on("error",(U)=>{clearTimeout(z);V(U)})});const Y=Date.now()+5000;while(Date.now()<Y){if(!await q(K.port,500))break;await new Promise((Z)=>setTimeout(Z,200))}if(process.platform!=="win32"){const Z=W.join(K.userDataDir,"SingletonLock"),V=Date.now()+15000;while(Date.now()<V){try{B.lstatSync(Z)}catch{return!0}await new Promise(($)=>setTimeout($,200))}J.warn(`[managed-chrome] Chrome 端口已释放但 15s 内 SingletonLock 未消失:${Z}`)}return!0}async function h(K){return!await q(K,500)}async function l(){const K=new Set(_().map(({entry:H})=>H.port));for(let H=j;H<=k;H++){if(K.has(H))continue;if(await h(H))return H}throw Error(`托管 profile 端口区间 ${j}-${k} 已耗尽`)}export async function resolveManagedOpts(K){const H={};if(K.label)H.label=K.label;if(K.proxy)H.proxy=K.proxy;if(K.webrtc===!1)H.webrtc=!1;if(K.clean===!1)H.clean=!1;if(K.fingerprint?.enabled){const Q=K.fingerprint,Y=Q.seed??P();H.fingerprint=E(Y,{timezone:Q.timezone,locale:Q.locale,userAgent:Q.userAgent,platform:Q.platform,spoof:Q.spoof,hardwareConcurrency:Q.hardwareConcurrency,deviceMemory:Q.deviceMemory,screen:Q.screen,webglVendor:Q.webglVendor,webglRenderer:Q.webglRenderer,canvasNoise:Q.canvasNoise})}return{opts:H}}export async function createManagedProfile(K,H={}){const Q=await l(),Y=W.join(w(),K.trim()),Z={userDataDir:Y,port:Q,...H.browserPath?{browserPath:H.browserPath}:{},...H.label?{label:H.label}:{},...H.proxy?{proxy:H.proxy}:{},...H.fingerprint?{fingerprint:H.fingerprint}:{},...H.webrtc===!1?{webrtc:!1}:{},...H.clean===!1?{clean:!1}:{},createdAt:new Date().toISOString()};M(K,Z);B.rmSync(Y,{recursive:!0,force:!0});B.mkdirSync(Y,{recursive:!0});return Z}export async function updateManagedProfileConfig(K,H){const Q=N(K);if(!Q)throw Error(`托管 profile "${K}" 不存在`);const Y=H.webrtc!==void 0?H.webrtc:Q.webrtc,Z=H.clean!==void 0?H.clean:Q.clean,V={userDataDir:Q.userDataDir,port:Q.port,...Q.browserPath?{browserPath:Q.browserPath}:{},...Q.createdAt?{createdAt:Q.createdAt}:{},...H.label!==void 0?H.label?{label:H.label}:{}:Q.label?{label:Q.label}:{},...H.proxy!==void 0?H.proxy?{proxy:H.proxy}:{}:Q.proxy?{proxy:Q.proxy}:{},...H.fingerprint!==void 0?H.fingerprint?{fingerprint:H.fingerprint}:{}:Q.fingerprint?{fingerprint:Q.fingerprint}:{},...Y===!1?{webrtc:!1}:{},...Z===!1?{clean:!1}:{}};let $=!1;try{$=await stopManagedChrome(Q)}catch(z){J.warn(`[managed-chrome] 更新前关闭 profile「${K}」失败(继续):${z instanceof Error?z.message:z}`)}A(K,V);return{entry:V,wasRunning:$}}async function c(K){const H=Date.now()+15000;let Q=0;while(Date.now()<H){try{B.rmSync(K,{recursive:!0,force:!0,maxRetries:3,retryDelay:100})}catch{}await new Promise((Y)=>setTimeout(Y,300));if(!B.existsSync(K)){if(++Q>=2)return}else Q=0}throw new O(`数据目录未能删净(浏览器可能仍在退出中):${K}`,"等浏览器窗口完全关闭后再删一次即可。")}export async function removeManagedProfile(K,H={}){const Q=N(K);if(!Q)throw Error(`托管 profile "${K}" 不存在`);try{await stopManagedChrome(Q)}catch(Y){J.warn(`[managed-chrome] 关闭 profile「${K}」失败(继续删除):${Y instanceof Error?Y.message:Y}`)}if(!H.keepData){const Y=w(),Z=W.resolve(Q.userDataDir);if(Z.startsWith(Y+W.sep))await c(Z);else J.warn(`[managed-chrome] user-data-dir 不在 ${Y} 下,跳过数据删除:${Z}`)}b(K)}
|
|
1
|
+
import{execFileSync as P}from"node:child_process";import*as B from"node:fs";import*as q from"node:path";import{request as f}from"node:http";import{WebSocket as L}from"ws";import{probeCDP as I,launchDetachedApp as w}from"../launcher.js";import{withBrowserSessionLock as C}from"../browser-session-lock.js";import{CommandExecutionError as F}from"../errors.js";import{log as U}from"../logger.js";import{addManagedProfile as y,getManagedProfile as _,listManagedProfiles as u,managedProfilesBaseDir as j,removeManagedProfileEntry as g,updateManagedProfile as h}from"./profile.js";import{proxyServerFlag as m}from"./proxy-test.js";import{resolveFingerprint as l,randomSeed as c}from"./fingerprint.js";const d=500,v=20000,A=9301,b=9399;function M(K){for(const Q of K)try{B.accessSync(Q,B.constants.X_OK);return Q}catch{}return null}function i(K){for(const Q of K)try{const Y=P("which",[Q],{encoding:"utf-8",stdio:"pipe"}).trim();if(Y)return Y}catch{}return null}export function discoverChromePath(){const K=process.env.OPENCLI_CHROME_PATH?.trim();if(K)return K;if(process.platform==="darwin")return M(["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]);if(process.platform==="win32"){const Q=process.env.ProgramFiles??"C:\\Program Files",Y=process.env["ProgramFiles(x86)"]??"C:\\Program Files (x86)",Z=process.env.LOCALAPPDATA??"";return M([q.join(Q,"Google","Chrome","Application","chrome.exe"),q.join(Y,"Google","Chrome","Application","chrome.exe"),...Z?[q.join(Z,"Google","Chrome","Application","chrome.exe")]:[],q.join(Q,"Microsoft","Edge","Application","msedge.exe"),q.join(Y,"Microsoft","Edge","Application","msedge.exe")])}return i(["google-chrome-stable","google-chrome","chromium","chromium-browser","microsoft-edge-stable"])}function x(K,Q,Y=5000){return new Promise((Z,H)=>{const X=new URL(Q),$=f({hostname:X.hostname,port:X.port,path:X.pathname+X.search,method:K,timeout:Y},(V)=>{const J=[];V.on("data",(z)=>J.push(Buffer.isBuffer(z)?z:Buffer.from(z)));V.on("end",()=>{const z=Buffer.concat(J).toString("utf8");if((V.statusCode??0)<200||(V.statusCode??0)>=300){H(Error(`HTTP ${V.statusCode}: ${z.slice(0,200)}`));return}try{Z(z?JSON.parse(z):null)}catch{Z(z)}})});$.on("error",H);$.on("timeout",()=>$.destroy(Error(`请求 ${Q} 超时`)));$.end()})}export function managedEndpoint(K){return`http://127.0.0.1:${K.port}`}export async function isManagedChromeRunning(K){return I(K.port)}const p=["--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-domain-reliability","--disable-breakpad","--disable-client-side-phishing-detection","--no-pings","--no-service-autorun","--password-store=basic","--disable-features=Translate,OptimizationHints,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions,CalculateNativeWinOcclusion"];function o(K){const Q=[`--user-data-dir=${K.userDataDir}`,`--remote-debugging-port=${K.port}`,"--remote-allow-origins=*"];if(K.clean!==!1)Q.push(...p);if(K.webrtc!==!1)Q.push("--force-webrtc-ip-handling-policy=disable_non_proxied_udp");if(K.proxy)Q.push(`--proxy-server=${m(K.proxy)}`);if(K.fingerprint?.enabled&&K.fingerprint.locale)Q.push(`--lang=${K.fingerprint.locale}`);Q.push("--no-startup-window");return Q}function s(K){const Q=K.fingerprint?.enabled?K.fingerprint.timezone:void 0;return Q?{TZ:Q}:void 0}async function a(K,Q){const Y=Date.now()+v;while(Date.now()<Y){if(await I(K,1000))return;await new Promise((Z)=>setTimeout(Z,d))}throw new F(`托管 Chrome「${Q}」已启动但 ${v/1000}s 内 CDP 端口 ${K} 未就绪`,"浏览器可能启动缓慢,稍后重试;或检查该端口是否被其它进程占用。")}const t="https://publishport.app/automation";function n(K){if(!K)return!1;try{const Q=new URL(K);return(Q.hostname==="publishport.app"||Q.hostname==="www.publishport.app")&&Q.pathname.endsWith("/automation")}catch{return!1}}async function T(K,Q){const Z=(await x("GET",`${K}/json/version`))?.webSocketDebuggerUrl;if(!Z)throw Error(`CDP 端点 ${K} 没有暴露 browser target`);const H=new L(Z);await new Promise((J,z)=>{const W=setTimeout(()=>{H.close();z(Error("browser CDP 连接超时"))},1e4);H.on("open",()=>{clearTimeout(W);J()});H.on("error",(O)=>{clearTimeout(W);z(O)})});let X=0;const $=new Map;H.on("message",(J)=>{try{const z=JSON.parse(String(J));if(!z.id||!$.has(z.id))return;const W=$.get(z.id);$.delete(z.id);if(z.error)W.reject(Error(z.error.message??"CDP error"));else W.resolve(z.result??{})}catch{}});const V=(J,z={})=>{const W=++X;return new Promise((O,G)=>{const k=setTimeout(()=>{$.delete(W);G(Error(`CDP ${J} 超时`))},1e4);$.set(W,{resolve:(S)=>{clearTimeout(k);O(S)},reject:(S)=>{clearTimeout(k);G(S)}});H.send(JSON.stringify({id:W,method:J,params:z}))})};try{return await Q(V)}finally{H.close()}}async function D(K){const Q=await x("GET",`${K}/json`);return Array.isArray(Q)?Q.filter((Y)=>Y.type==="page"):[]}async function R(K,Q,Y){try{let Z=await D(K);if(Z.length===0){await C(async()=>{if((await D(K)).length>0)return;await T(K,async($)=>{const J=(await $("Target.createTarget",{url:t,background:!0})).targetId;if(J&&Y==="background"){const z=await $("Browser.getWindowForTarget",{targetId:J});if(z.windowId!==void 0)for(let W=0;W<4;W++){await $("Browser.setWindowBounds",{windowId:z.windowId,bounds:{windowState:"minimized"}});await new Promise((G)=>setTimeout(G,250));if((await $("Browser.getWindowBounds",{windowId:z.windowId})).bounds?.windowState==="minimized")break}}})},`anchor-${Q}`);return}const H=Z.filter(($)=>n($.url));if(H.length>1){await T(K,async($)=>{for(const V of H.slice(1))if(V.id)await $("Target.closeTarget",{targetId:V.id})});Z=Z.filter(($)=>!H.slice(1).some((V)=>V.id===$.id))}const X=Z[0];if(Y==="foreground"&&X?.id)await T(K,async($)=>{const V=await $("Browser.getWindowForTarget",{targetId:X.id});if((await $("Browser.getWindowBounds",{windowId:V.windowId})).bounds?.windowState==="minimized")await $("Browser.setWindowBounds",{windowId:V.windowId,bounds:{windowState:"normal"}})})}catch(Z){U.debug(`[managed-chrome] ensurePageTarget 失败(非致命):${Z instanceof Error?Z.message:Z}`)}}const N=new Map;async function E(K){if(await I(K.port))return!0;if(process.platform==="win32")return!1;const Q=q.join(K.userDataDir,"SingletonLock"),Y=()=>{try{B.lstatSync(Q);return!0}catch{return!1}};if(!Y())return!1;for(let Z=0;Z<3;Z++){await new Promise((H)=>setTimeout(H,500));if(await I(K.port,4000))return!0;if(!Y())return!1}return!1}export async function ensureManagedChrome(K,Q,Y={}){const Z=managedEndpoint(Q),H=N.get(K);if(H)return H;if(await E(Q)){U.debug(`[managed-chrome] profile「${K}」已在端口 ${Q.port} 运行`);await R(Z,K,Y.windowMode);return Z}const X=N.get(K);if(X)return X;const $=C(async()=>{if(await E(Q)){U.debug(`[managed-chrome] profile「${K}」等锁期间已被拉起,直接复用`);await R(Z,K,Y.windowMode);return Z}const V=Q.browserPath??discoverChromePath();if(!V)throw new F("找不到可用的 Chrome/Chromium 浏览器。","安装 Google Chrome,或用 OPENCLI_CHROME_PATH 环境变量指定可执行文件路径。");B.mkdirSync(Q.userDataDir,{recursive:!0});U.debug(`[managed-chrome] 拉起 profile「${K}」:${V} 端口 ${Q.port}${Q.proxy?" (代理)":""}`);await w(V,o(Q),`托管 profile ${K}`,s(Q));await a(Q.port,K);await R(Z,K,Y.windowMode);return Z},`launch-${K}`).finally(()=>N.delete(K));N.set(K,$);return $}export async function stopManagedChrome(K){if(!await I(K.port))return!1;const Y=(await x("GET",`${managedEndpoint(K)}/json/version`))?.webSocketDebuggerUrl;if(!Y)throw Error(`端口 ${K.port} 上的 CDP 端点没有暴露 browser target`);await new Promise((H,X)=>{const $=new L(Y),V=setTimeout(()=>{$.close();X(Error("CDP Browser.close 超时"))},1e4);$.on("open",()=>{$.send(JSON.stringify({id:1,method:"Browser.close",params:{}}))});$.on("close",()=>{clearTimeout(V);H()});$.on("error",(J)=>{clearTimeout(V);X(J)})});const Z=Date.now()+5000;while(Date.now()<Z){if(!await I(K.port,500))break;await new Promise((H)=>setTimeout(H,200))}if(process.platform!=="win32"){const H=q.join(K.userDataDir,"SingletonLock"),X=Date.now()+15000;while(Date.now()<X){try{B.lstatSync(H)}catch{return!0}await new Promise(($)=>setTimeout($,200))}U.warn(`[managed-chrome] Chrome 端口已释放但 15s 内 SingletonLock 未消失:${H}`)}return!0}async function r(K){return!await I(K,500)}async function e(){const K=new Set(u().map(({entry:Q})=>Q.port));for(let Q=A;Q<=b;Q++){if(K.has(Q))continue;if(await r(Q))return Q}throw Error(`托管 profile 端口区间 ${A}-${b} 已耗尽`)}export async function resolveManagedOpts(K){const Q={};if(K.label)Q.label=K.label;if(K.proxy)Q.proxy=K.proxy;if(K.webrtc===!1)Q.webrtc=!1;if(K.clean===!1)Q.clean=!1;if(K.fingerprint?.enabled){const Y=K.fingerprint,Z=Y.seed??c();Q.fingerprint=l(Z,{timezone:Y.timezone,locale:Y.locale,userAgent:Y.userAgent,platform:Y.platform,spoof:Y.spoof,hardwareConcurrency:Y.hardwareConcurrency,deviceMemory:Y.deviceMemory,screen:Y.screen,webglVendor:Y.webglVendor,webglRenderer:Y.webglRenderer,canvasNoise:Y.canvasNoise})}return{opts:Q}}export async function createManagedProfile(K,Q={}){const Y=await e(),Z=q.join(j(),K.trim()),H={userDataDir:Z,port:Y,...Q.browserPath?{browserPath:Q.browserPath}:{},...Q.label?{label:Q.label}:{},...Q.proxy?{proxy:Q.proxy}:{},...Q.fingerprint?{fingerprint:Q.fingerprint}:{},...Q.webrtc===!1?{webrtc:!1}:{},...Q.clean===!1?{clean:!1}:{},createdAt:new Date().toISOString()};y(K,H);B.rmSync(Z,{recursive:!0,force:!0});B.mkdirSync(Z,{recursive:!0});return H}export async function updateManagedProfileConfig(K,Q){const Y=_(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);const Z=Q.webrtc!==void 0?Q.webrtc:Y.webrtc,H=Q.clean!==void 0?Q.clean:Y.clean,X={userDataDir:Y.userDataDir,port:Y.port,...Y.browserPath?{browserPath:Y.browserPath}:{},...Y.createdAt?{createdAt:Y.createdAt}:{},...Q.label!==void 0?Q.label?{label:Q.label}:{}:Y.label?{label:Y.label}:{},...Q.proxy!==void 0?Q.proxy?{proxy:Q.proxy}:{}:Y.proxy?{proxy:Y.proxy}:{},...Q.fingerprint!==void 0?Q.fingerprint?{fingerprint:Q.fingerprint}:{}:Y.fingerprint?{fingerprint:Y.fingerprint}:{},...Z===!1?{webrtc:!1}:{},...H===!1?{clean:!1}:{}};let $=!1;try{$=await stopManagedChrome(Y)}catch(V){U.warn(`[managed-chrome] 更新前关闭 profile「${K}」失败(继续):${V instanceof Error?V.message:V}`)}h(K,X);return{entry:X,wasRunning:$}}async function KK(K){const Q=Date.now()+15000;let Y=0;while(Date.now()<Q){try{B.rmSync(K,{recursive:!0,force:!0,maxRetries:3,retryDelay:100})}catch{}await new Promise((Z)=>setTimeout(Z,300));if(!B.existsSync(K)){if(++Y>=2)return}else Y=0}throw new F(`数据目录未能删净(浏览器可能仍在退出中):${K}`,"等浏览器窗口完全关闭后再删一次即可。")}export async function removeManagedProfile(K,Q={}){const Y=_(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);try{await stopManagedChrome(Y)}catch(Z){U.warn(`[managed-chrome] 关闭 profile「${K}」失败(继续删除):${Z instanceof Error?Z.message:Z}`)}if(!Q.keepData){const Z=j(),H=q.resolve(Y.userDataDir);if(H.startsWith(Z+q.sep))await KK(H);else U.warn(`[managed-chrome] user-data-dir 不在 ${Z} 下,跳过数据删除:${H}`)}g(K)}
|