publishport-opencli 1.0.9 → 1.0.11
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 +42 -86
- 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 G,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 W(z){if(z&&typeof z==="object"&&typeof z.session==="string"&&Object.prototype.hasOwnProperty.call(z,"data"))return z.data;return z}function Iz(z){return z.map((J)=>{const K=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,34 +79,30 @@ 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
|
|
123
|
-
((selector
|
|
82
|
+
`);let q;if(Z.kind==="contenteditable"&&z.insertText){if(!(await z.evaluate(`
|
|
83
|
+
((selector) => {
|
|
124
84
|
const __opencli_xhs_fill_phase = "prepare";
|
|
125
|
-
const fireBeforeInput = (el, value) => {
|
|
126
|
-
try {
|
|
127
|
-
el.dispatchEvent(new InputEvent('beforeinput', {
|
|
128
|
-
bubbles: true,
|
|
129
|
-
data: value,
|
|
130
|
-
inputType: 'insertText',
|
|
131
|
-
}));
|
|
132
|
-
} catch {
|
|
133
|
-
el.dispatchEvent(new Event('beforeinput', { bubbles: true }));
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
85
|
const el = Array.from(document.querySelectorAll(selector)).find(node => node && node.offsetParent !== null);
|
|
137
86
|
if (!el) return { ok: false };
|
|
138
87
|
el.focus();
|
|
139
|
-
el.textContent = '';
|
|
140
88
|
const selection = window.getSelection();
|
|
141
89
|
const range = document.createRange();
|
|
142
90
|
range.selectNodeContents(el);
|
|
91
|
+
selection?.removeAllRanges();
|
|
92
|
+
selection?.addRange(range);
|
|
93
|
+
if ((el.innerText || el.textContent || '').trim()) {
|
|
94
|
+
const deleted = document.execCommand('delete', false);
|
|
95
|
+
if (!deleted) el.textContent = '';
|
|
96
|
+
}
|
|
97
|
+
range.selectNodeContents(el);
|
|
143
98
|
range.collapse(false);
|
|
144
99
|
selection?.removeAllRanges();
|
|
145
100
|
selection?.addRange(range);
|
|
146
|
-
fireBeforeInput(el, nextText);
|
|
147
101
|
return { ok: true };
|
|
148
|
-
})(${JSON.stringify(Z.sel)}
|
|
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{
|
|
102
|
+
})(${JSON.stringify(Z.sel)})
|
|
103
|
+
`))?.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`)}await z.wait({time:0.35});try{const X=String(K).split(`
|
|
104
|
+
`);for(let j=0;j<X.length;j++){if(j>0)if(typeof z.pressKey==="function"){await z.pressKey("Enter");await z.wait({time:0.15})}else await z.insertText(`
|
|
105
|
+
`);if(X[j])await x(z,X[j])}q=await z.evaluate(`
|
|
150
106
|
((selector, expectedText) => {
|
|
151
107
|
const __opencli_xhs_fill_phase = "verify";
|
|
152
108
|
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
@@ -169,7 +125,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
169
125
|
const actual = normalize(el.innerText || el.textContent || '');
|
|
170
126
|
return { ok: actual === normalize(expectedText), actual };
|
|
171
127
|
})(${JSON.stringify(Z.sel)}, ${JSON.stringify(K)})
|
|
172
|
-
`)}catch{
|
|
128
|
+
`)}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 W(await z.evaluate(`
|
|
173
129
|
(selectors => {
|
|
174
130
|
const el = selectors
|
|
175
131
|
.map(sel => Array.from(document.querySelectorAll(sel)))
|
|
@@ -185,7 +141,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
185
141
|
selection?.addRange(range);
|
|
186
142
|
return true;
|
|
187
143
|
})(${JSON.stringify(J)})
|
|
188
|
-
`))}function
|
|
144
|
+
`))}function az(z,{click:J=!1}={}){return`
|
|
189
145
|
(topicName => {
|
|
190
146
|
const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
|
|
191
147
|
const want = norm(topicName);
|
|
@@ -225,7 +181,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
225
181
|
text: (target.innerText || target.textContent || '').trim().slice(0, 40),
|
|
226
182
|
};
|
|
227
183
|
})(${JSON.stringify(z)})
|
|
228
|
-
`}function
|
|
184
|
+
`}function ez(z,J){return`
|
|
229
185
|
((topicName, selectors) => {
|
|
230
186
|
const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
|
|
231
187
|
const want = norm(topicName);
|
|
@@ -256,7 +212,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
256
212
|
}
|
|
257
213
|
return count;
|
|
258
214
|
})(${JSON.stringify(z)}, ${JSON.stringify(J)})
|
|
259
|
-
`}function
|
|
215
|
+
`}function Kz(z,J){return`
|
|
260
216
|
((topicName, selectors) => {
|
|
261
217
|
const __opencli_xhs_topic_marker_count = true;
|
|
262
218
|
const marker = '#' + topicName + '[话题]';
|
|
@@ -274,14 +230,14 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
274
230
|
}
|
|
275
231
|
return count;
|
|
276
232
|
})(${JSON.stringify(z)}, ${JSON.stringify(J)})
|
|
277
|
-
`}async function
|
|
233
|
+
`}async function zJ(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return W(await z.evaluate(`
|
|
278
234
|
(text => {
|
|
279
235
|
const ok = document.execCommand('insertText', false, text);
|
|
280
236
|
const active = document.activeElement;
|
|
281
237
|
if (active) active.dispatchEvent(new Event('input', { bubbles: true }));
|
|
282
238
|
return ok;
|
|
283
239
|
})(${JSON.stringify(K)})
|
|
284
|
-
`))}function
|
|
240
|
+
`))}function Qz(z){return`
|
|
285
241
|
(selectors => {
|
|
286
242
|
let editor = null;
|
|
287
243
|
for (const sel of selectors) {
|
|
@@ -291,7 +247,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
291
247
|
if (!editor) return -1;
|
|
292
248
|
return (editor.innerText || editor.textContent || '').length;
|
|
293
249
|
})(${JSON.stringify(z)})
|
|
294
|
-
`}async function Lz(z,J,K){if(!await Mz(z,J))throw new
|
|
250
|
+
`}async function Lz(z,J,K){if(!await Mz(z,J))throw new G(`Could not attach topic "${K}": body editor not found`);const Z=Number(W(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 G(`Could not attach topic "${K}": page.insertText is unavailable`);try{await x(z,`#${K}`)}catch{throw new G(`Could not attach topic "${K}": failed to type inline topic query`)}await N(z,1.2);if(typeof z.pressKey!=="function")throw new G(`Could not attach topic "${K}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch(q){throw new G(`Could not attach topic "${K}": failed to accept suggestion (${q&&q.message||q})`)}await z.wait({time:0.8});if((Number(W(await z.evaluate(Kz(K,J))))||0)<=Z)throw new G(`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(W(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 X=$.length+6;for(let j=0;j<X;j++){const O=Number(W(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
251
|
() => {
|
|
296
252
|
const isVisible = (el) => {
|
|
297
253
|
if (!el || el.offsetParent === null) return false;
|
|
@@ -340,7 +296,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
340
296
|
}
|
|
341
297
|
return { ok: false, visibleTexts };
|
|
342
298
|
}
|
|
343
|
-
`);if(J?.ok)await z.wait({time:1});return J}async function
|
|
299
|
+
`);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++){$=W(await z.evaluate(`
|
|
344
300
|
((cfg) => {
|
|
345
301
|
const __opencli_xhs_click_label = { wantLabel: ${JSON.stringify(J)} };
|
|
346
302
|
const wantLabel = ${JSON.stringify(J)};
|
|
@@ -375,7 +331,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
375
331
|
}
|
|
376
332
|
return { ok: false };
|
|
377
333
|
})()
|
|
378
|
-
`));if(
|
|
334
|
+
`));if($?.ok)return $;await z.wait({time:Q/1000})}return $}async function Az(z){const J=await z.evaluate(`
|
|
379
335
|
(() => {
|
|
380
336
|
const __opencli_xhs_focus_card = true;
|
|
381
337
|
const sel = ${JSON.stringify(u)};
|
|
@@ -392,7 +348,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
392
348
|
selection?.addRange(range);
|
|
393
349
|
return { ok: true };
|
|
394
350
|
})()
|
|
395
|
-
`);return W(J)}async function
|
|
351
|
+
`);return W(J)}async function Rz(z){const J=await z.evaluate(`
|
|
396
352
|
(() => {
|
|
397
353
|
const __opencli_xhs_card_text = true;
|
|
398
354
|
const sel = ${JSON.stringify(u)};
|
|
@@ -401,7 +357,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
401
357
|
const text = active ? (active.innerText || active.textContent || '').trim() : '';
|
|
402
358
|
return { ok: !!text, text };
|
|
403
359
|
})()
|
|
404
|
-
`);return W(J)}async function
|
|
360
|
+
`);return W(J)}async function Zz(z){const J=await z.evaluate(`
|
|
405
361
|
(() => {
|
|
406
362
|
const __opencli_xhs_card_count = true;
|
|
407
363
|
const sel = ${JSON.stringify(u)};
|
|
@@ -410,16 +366,16 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
410
366
|
const activeText = active ? (active.innerText || active.textContent || '').trim() : '';
|
|
411
367
|
return { ok: true, count: editors.length, activeEmpty: !activeText };
|
|
412
368
|
})()
|
|
413
|
-
`);return W(J)}async function
|
|
369
|
+
`);return W(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
370
|
(() => {
|
|
415
371
|
const __opencli_xhs_preview_ready = true;
|
|
416
372
|
const ready = Array.from(document.querySelectorAll('button'))
|
|
417
373
|
.some((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '下一步');
|
|
418
374
|
return { ok: ready };
|
|
419
375
|
})()
|
|
420
|
-
`);return W(J)}async function Cz(z,J=6){const K=400,Q=Math.ceil(3000/K);for(let Z=0;Z<J;Z++){if(!(await
|
|
421
|
-
`);if(!(await
|
|
422
|
-
`);for(let
|
|
376
|
+
`);return W(J)}async function Cz(z,J=6){const K=400,Q=Math.ceil(3000/K);for(let Z=0;Z<J;Z++){if(!(await 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,`
|
|
377
|
+
`);if(!(await Az(z))?.ok)throw new G(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const $=J.split(`
|
|
378
|
+
`);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 G(`文字配图: card editor #${K+1} is empty after typing`)}async function xz(z,J){if(!J||J===I)return I;const K=W(await z.evaluate(`
|
|
423
379
|
(async () => {
|
|
424
380
|
const __opencli_xhs_card_styles = true;
|
|
425
381
|
const want = ${JSON.stringify(J)};
|
|
@@ -448,7 +404,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
448
404
|
}
|
|
449
405
|
return { ok: seen.length > 0, styles: seen, found: false };
|
|
450
406
|
})()
|
|
451
|
-
`));if(!K?.found)throw new
|
|
407
|
+
`));if(!K?.found)throw new G(`文字配图: 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 G(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function $z(z){const J=await z.evaluate(`
|
|
452
408
|
(() => {
|
|
453
409
|
const __opencli_xhs_composer_media_count = true;
|
|
454
410
|
const visibleBox = (el) => {
|
|
@@ -480,7 +436,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
480
436
|
}
|
|
481
437
|
return { ok: true, count };
|
|
482
438
|
})()
|
|
483
|
-
`);return W(J)}async function
|
|
439
|
+
`);return W(J)}async function qz(z,J,K){const Q=await $z(z);if(!Q||typeof Q.count!=="number")throw new G(`${K}: could not verify current composer media count`);if(Q.count<J){await z.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new G(`${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 G(`文字配图: 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 G(`文字配图: 写文字 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 G(`文字配图: 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 G(`文字配图: "${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 G(`文字配图: 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
440
|
() => {
|
|
485
441
|
const text = (document.body?.innerText || '').replace(/s+/g, ' ').trim();
|
|
486
442
|
const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
|
|
@@ -510,7 +466,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
510
466
|
const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
|
|
511
467
|
return { state, hasTitleInput, hasImageInput, hasVideoSurface };
|
|
512
468
|
}
|
|
513
|
-
`)}async function
|
|
469
|
+
`)}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
470
|
(() => {
|
|
515
471
|
const sels = ${JSON.stringify(m)};
|
|
516
472
|
for (const sel of sels) {
|
|
@@ -518,7 +474,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
518
474
|
if (el && el.offsetParent !== null) return true;
|
|
519
475
|
}
|
|
520
476
|
return false;
|
|
521
|
-
})()`))return!0;if(Z<Q-1)await z.wait({time:K/1000})}return!1}async function
|
|
477
|
+
})()`))return!0;if(Z<Q-1)await z.wait({time:K/1000})}return!1}async function cz(z){const J=W(await z.evaluate(`
|
|
522
478
|
(() => {
|
|
523
479
|
const __opencli_xhs_restore_srcs = true;
|
|
524
480
|
return Array.from(document.querySelectorAll('.img-container img'))
|
|
@@ -556,7 +512,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
556
512
|
}
|
|
557
513
|
return removed;
|
|
558
514
|
})(${JSON.stringify(J)})
|
|
559
|
-
`));return Number(K)||0}function
|
|
515
|
+
`));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),X=new Set;for(let j=0;j<V;j++){const O=await z.readNetworkCapture().catch(()=>[]);for(const D of Array.isArray(O)?O:[]){const Y=dz(D);if(Y)X.add(Y)}if(X.size>=J)return{ok:!0,completed:X.size};if(j>=q&&X.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:X.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{$=W(await z.evaluate(`(() => {
|
|
560
516
|
const noteIdRe = /"noteId":"([0-9a-f]{24})"/;
|
|
561
517
|
return Array.from(document.querySelectorAll('div.note[data-impression], div.note')).slice(0, 3).map((card) => {
|
|
562
518
|
const impression = card.getAttribute('data-impression') || '';
|
|
@@ -565,7 +521,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
565
521
|
const dateText = (card.querySelector('.time_status, .time')?.innerText || '').trim();
|
|
566
522
|
return { id, title, dateText };
|
|
567
523
|
}).filter((entry) => entry.title);
|
|
568
|
-
})()`))}catch{return null}if(Array.isArray(
|
|
524
|
+
})()`))}catch{return null}if(Array.isArray($)){const q=$.find((V)=>{const X=String(V.title||"").trim();if(!(X===Q||X.startsWith(Q)||Q.startsWith(X)))return!1;const j=String(V.dateText||"").match(/(\d{4})年(\d{2})月(\d{2})日 (\d{2}):(\d{2})/);if(!j)return!0;const O=Date.UTC(Number(j[1]),Number(j[2])-1,Number(j[3]),Number(j[4]),Number(j[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),X=J["card-text"]?String(J["card-text"]):"",j=X?X.split(o).map((H)=>H.trim()).filter(Boolean):[],O=J["card-style"]?String(J["card-style"]).trim():"",D=j.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 Y=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,j,O);else{const H=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,B=await e(z,Y);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new G(`图片注入失败:${B.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await z.wait({time:i/1000});const v=await Gz(z,Y.length,H);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new G(`图片上传未全部完成:只确认 ${v.completed}/${Y.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>Y.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new G(`清掉恢复态旧图后编辑器内仍有 ${A.count} 张图,超过本次注入的 ${Y.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 G("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,j.length,"文字配图 generated images");if(D&&Y.length>0){const H=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,B=await e(z,Y);if(!B.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new G(`追加图片失败:${B.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await z.wait({time:i/1000});const v=await Gz(z,Y.length,H);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new G(`追加图片上传未全部完成:只确认 ${v.completed}/${Y.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await zz(z);await qz(z,j.length+Y.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
525
|
(cfg => {
|
|
570
526
|
const { isDraftMode, publishNames, draftNames, labels } = cfg;
|
|
571
527
|
const isVisible = (el) => {
|
|
@@ -604,7 +560,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
604
560
|
}
|
|
605
561
|
}
|
|
606
562
|
return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
|
|
607
|
-
})(${JSON.stringify({isDraftMode:V,publishNames:
|
|
563
|
+
})(${JSON.stringify({isDraftMode:V,publishNames:Pz,draftNames:kz,labels:c})})
|
|
608
564
|
`);if(!F?.ok){if(V){if(await z.evaluate(`
|
|
609
565
|
(() => {
|
|
610
566
|
const labels = ['返回', '关闭', '取消', '离开'];
|
|
@@ -643,7 +599,7 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
643
599
|
}
|
|
644
600
|
return false;
|
|
645
601
|
}
|
|
646
|
-
`)){F.ok=!0;F.via="auto-save"}}}}if(!F?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const
|
|
602
|
+
`)){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
603
|
(markers => {
|
|
648
604
|
for (const el of document.querySelectorAll('*')) {
|
|
649
605
|
if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
|
|
@@ -653,4 +609,4 @@ import*as w from"node:fs";import*as I from"node:path";import{CommandExecutionErr
|
|
|
653
609
|
}
|
|
654
610
|
return '';
|
|
655
611
|
})(${JSON.stringify(Xz)})
|
|
656
|
-
`),
|
|
612
|
+
`),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 G(`${C}无法确认:未观察到成功提示或提交后跳转。`+(k?`当前 URL: ${k}。`:"当前 URL 为空。")+"先用 `xiaohongshu drafts` 查草稿箱确认是否已存上;确认没有再重新运行本命令,避免堆出重复草稿。");throw new G(`${C}无法确认:未观察到成功提示或提交后跳转,创作者后台也暂未查到同标题笔记。`+(k?`当前 URL: ${k}。`:"")+"注意:发布按钮已被触发,笔记仍可能延迟入库。禁止立即重发——请等 1-2 分钟后用 `xiaohongshu creator-notes` 回查本篇标题是否已出现;确认不存在后才重新运行本 publish 命令,否则会重复发布。")}return[{status:`✅ ${C}`,detail:[`"${K}"`,D?`${j.length}张文字配图${Y.length?` + ${Y.length}张图片`:""}${f&&f!==I?` (${f})`:""}`:`${Y.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)}})()";
|