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