dsh-moli 1.1.11 → 1.1.12
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/README.md +71 -10
- package/README.zh.md +66 -7
- package/index.js +89 -28
- package/lib/actions.js +133 -53
- package/lib/session.js +373 -113
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -247,20 +247,70 @@ moli_session action=close # stop it and free the
|
|
|
247
247
|
Elements are addressed by **CSS selector**, never by pixel coordinates: the tool measures the element
|
|
248
248
|
and dispatches real mouse and key events, so page handlers run exactly as they do for a person.
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
and does not follow scrolling, and its hit test is bounded by the viewport height. Measured on Moli
|
|
252
|
-
1.1.16: an element at document `y=1008` reports the same `y` at `scrollY` 0, 400 and 800, while
|
|
253
|
-
`elementFromPoint` stops answering past `y=1080`. The consequence is that synthetic input cannot reach
|
|
254
|
-
an element below the first viewport, on any page long enough to scroll. `click` says so, naming the
|
|
255
|
-
element, and points at the route that does work:
|
|
250
|
+
### Choosing a browser: `moli` or `chromium`
|
|
256
251
|
|
|
252
|
+
`moli_session` can drive either engine, and you choose which — there is **no automatic fallback**,
|
|
253
|
+
because switching engines when one click fails would make the timing and the behaviour depend on which
|
|
254
|
+
operation happened to fail, and you could not tell which browser answered.
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
config:
|
|
258
|
+
sessionBrowser: moli # moli (default) or chromium
|
|
259
|
+
sessionBrowserPath: '' # optional path to chrome.exe / msedge.exe / brave
|
|
260
|
+
sessionPort: 9222
|
|
257
261
|
```
|
|
262
|
+
|
|
263
|
+
**Why the choice exists.** Moli's element geometry is document-absolute and does not follow scrolling,
|
|
264
|
+
and its hit test is bounded by the viewport height. Measured on Moli 1.1.16 and Chrome 155, same page,
|
|
265
|
+
same element:
|
|
266
|
+
|
|
267
|
+
| | `rect.y` at scrollY 0 / 400 / 800 | click a control below the first viewport |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| **moli** | 1008 / 1008 / 1008 — never moves | **impossible**; synthetic input cannot reach it |
|
|
270
|
+
| **chromium** | 1008 / 608 / 208 — follows the scroll | **works**; the browser scrolls to it |
|
|
271
|
+
|
|
272
|
+
So moli is the good default — it is the engine this plugin wraps, it is lighter, and it is already
|
|
273
|
+
installed — and a real Chromium is what you switch to when a task needs an element further down a long
|
|
274
|
+
page. When moli refuses, it says so, names the element, and points at both routes:
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
moli_session action=click selector="#far-down"
|
|
278
|
+
→ "#far-down" is below the viewport (y=2548) and this engine cannot send input there
|
|
279
|
+
|
|
280
|
+
# either click from inside the page (reaches it, but skips hover and focus behaviour)
|
|
258
281
|
moli_session action=eval expression="document.querySelector('#far-down').click()"
|
|
282
|
+
|
|
283
|
+
# or use a real browser for that session
|
|
284
|
+
config: sessionBrowser: chromium
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
With `chromium` the plugin starts the browser it finds (Chrome, Edge or Brave, or `sessionBrowserPath`)
|
|
288
|
+
in `--headless=new` mode with its own profile, so it does not touch your normal browsing profile.
|
|
289
|
+
|
|
290
|
+
### Driving a browser you already have open
|
|
291
|
+
|
|
292
|
+
Pass `cdpEndpoint` to attach to a CDP port instead of starting anything. This is the only way to inherit
|
|
293
|
+
your own cookies and logins: start your browser with `--remote-debugging-port=9222`, then
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
moli_session action=open url=https://example.com cdpEndpoint=http://127.0.0.1:9222
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The endpoint applies to that call only. A browser this plugin did not start is **never stopped by
|
|
300
|
+
`close`**, and `status` reports `external: true` so you can tell.
|
|
301
|
+
|
|
302
|
+
### `cdp` — the raw protocol escape hatch
|
|
303
|
+
|
|
304
|
+
Everything the named actions do is an abstraction chosen in advance. `action=cdp` is not:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
moli_session action=cdp method=Browser.getVersion
|
|
308
|
+
moli_session action=cdp method=Network.setExtraHTTPHeaders params={"headers":{"X-Thing":"1"}}
|
|
309
|
+
moli_session action=cdp method=DOM.setFileInputFiles params={"files":["/abs/a.png"],"nodeId":42}
|
|
259
310
|
```
|
|
260
311
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
control and calling its handler.
|
|
312
|
+
It reaches what the wrapper does not: other targets, cross-origin frames, file inputs, extra headers,
|
|
313
|
+
network interception. `params` may be an object or a JSON string.
|
|
264
314
|
|
|
265
315
|
The session needs **Node 21 or newer** for the built-in `WebSocket`; on Node 20 the tool answers with
|
|
266
316
|
that reason instead of failing obscurely. `moli_fetch` and `moli_probe` still work on 20.12.
|
|
@@ -356,6 +406,8 @@ Override any field under the plugin's `config` entry in the profile patch layer.
|
|
|
356
406
|
maxChars: 200000 # inline text truncation
|
|
357
407
|
proxy: 'http://127.0.0.1:7890' # forward proxy for moli, downloads, and the installer's engine download
|
|
358
408
|
insecureTls: true # see the TLS notes below
|
|
409
|
+
sessionBrowser: moli # or chromium, for the session tool
|
|
410
|
+
sessionBrowserPath: '' # explicit chrome.exe / msedge.exe / brave
|
|
359
411
|
sessionPort: 9222 # CDP port for moli_session
|
|
360
412
|
blockPrivateNetworks: false # refuse private addresses (see the note below)
|
|
361
413
|
blockCidrs: [] # extra CIDRs to refuse
|
|
@@ -441,7 +493,16 @@ Four suites live in [`verify/`](verify/), and all are runnable against a working
|
|
|
441
493
|
with and without clearing, press, scroll, `waitFor` in both directions, eval including a throwing
|
|
442
494
|
expression, screenshot byte signature, navigation by clicking a link, restart after close, and the
|
|
443
495
|
viewport limit described above — which is asserted as a limit, with the `eval` route that works
|
|
444
|
-
around it asserted next to it.
|
|
496
|
+
around it asserted next to it. It skips when no engine is installed.
|
|
497
|
+
- `session-backends.mjs` is the comparison that justifies the second backend: the same page, the same
|
|
498
|
+
control below the viewport, asserted to be **refused on moli and clicked on Chromium**, plus the
|
|
499
|
+
`cdp` action on both. The Chromium half skips when the machine has no Chromium-family browser.
|
|
500
|
+
- `session-attach-verify.mjs` covers external endpoints and ownership: attaching to a browser this
|
|
501
|
+
plugin did not start, identifying it correctly, refusing to stop it on `close`, and — the trap that
|
|
502
|
+
a behavioural probe got wrong — classifying an external **moli** as moli even though it answers
|
|
503
|
+
`/json/version` with `"Browser":"Chrome/145.0.0.0"`. It also asserts that naming a per-call endpoint
|
|
504
|
+
does not latch it, which an earlier version did: one call against a dead endpoint left the session
|
|
505
|
+
pointing at port 9 for the rest of the process.
|
|
445
506
|
|
|
446
507
|
`.github/workflows/verify.yml` runs the first two on Linux, Windows and macOS (they need no engine and
|
|
447
508
|
no network) and attempts the engine-dependent suites on Linux and Windows, reporting a missing engine
|
package/README.zh.md
CHANGED
|
@@ -231,17 +231,67 @@ moli_session action=close # 停止并释放端口
|
|
|
231
231
|
元素一律用 **CSS 选择器**定位,不接受像素坐标:工具自己测量元素并派发真实的鼠标与键盘事件,所以页面的
|
|
232
232
|
处理器会像人操作时一样执行。
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
**受视口高度限制**。在 Moli 1.1.16 上实测:文档 `y=1008` 的元素在 `scrollY` 为 0、400、800 时报告的 `y`
|
|
236
|
-
完全相同,而 `elementFromPoint` 在 `y=1080` 之后一律返回 null。后果是:**在长到需要滚动的页面上,合成输入
|
|
237
|
-
无法触达首个视口以下的元素。** `click` 会如实报出这一点并点名元素,同时给出真正可行的路径:
|
|
234
|
+
### 选哪个浏览器:`moli` 还是 `chromium`
|
|
238
235
|
|
|
236
|
+
`moli_session` 可以驱动两种引擎,由你选择——**没有自动降级**。因为"某个点击失败就换引擎"会让工具的耗时和
|
|
237
|
+
行为取决于哪一步恰好失败,而且你无法知道最后是哪个浏览器在回答。
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
config:
|
|
241
|
+
sessionBrowser: moli # moli(默认)或 chromium
|
|
242
|
+
sessionBrowserPath: '' # 可选,指定 chrome.exe / msedge.exe / brave
|
|
243
|
+
sessionPort: 9222
|
|
239
244
|
```
|
|
245
|
+
|
|
246
|
+
**为什么需要这个选择。** moli 的元素几何是**文档绝对坐标且不随滚动更新**,命中测试又受视口高度限制。在
|
|
247
|
+
Moli 1.1.16 与 Chrome 155 上,同一页面、同一元素实测:
|
|
248
|
+
|
|
249
|
+
| | `rect.y` 在 scrollY 0 / 400 / 800 | 点击首个视口以下的控件 |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| **moli** | 1008 / 1008 / 1008 —— 纹丝不动 | **做不到**,合成输入触达不了 |
|
|
252
|
+
| **chromium** | 1008 / 608 / 208 —— 跟随滚动 | **可以**,浏览器会滚动到它 |
|
|
253
|
+
|
|
254
|
+
所以 moli 是合适的默认值——它是本插件包装的引擎、更轻、而且你已经装了;而当任务需要长页面下方的元素时,
|
|
255
|
+
就换成真实 Chromium。moli 拒绝时会把原因、元素和两条出路都讲清楚:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
moli_session action=click selector="#far-down"
|
|
259
|
+
→ "#far-down" is below the viewport (y=2548) and this engine cannot send input there
|
|
260
|
+
|
|
261
|
+
# 要么在页面内部点击(能触达,但会跳过 hover 与 focus 行为)
|
|
240
262
|
moli_session action=eval expression="document.querySelector('#far-down').click()"
|
|
263
|
+
|
|
264
|
+
# 要么这个会话改用真实浏览器
|
|
265
|
+
config: sessionBrowser: chromium
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
选 `chromium` 时,插件会在 `--headless=new` 模式下用**独立 profile** 启动它找到的浏览器(Chrome、Edge 或
|
|
269
|
+
Brave,也可用 `sessionBrowserPath` 指定),不会碰你日常使用的浏览器配置。
|
|
270
|
+
|
|
271
|
+
### 驱动你自己已经开着的浏览器
|
|
272
|
+
|
|
273
|
+
传 `cdpEndpoint` 即可连接到已有的 CDP 端口,什么都不启动。这是**继承你自己 cookie 与登录态的唯一途径**:
|
|
274
|
+
用 `--remote-debugging-port=9222` 启动你的浏览器,然后
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
moli_session action=open url=https://example.com cdpEndpoint=http://127.0.0.1:9222
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
该端点**只对当次调用生效**。不是本插件启动的浏览器**永远不会被 `close` 关掉**,并且 `status` 会报
|
|
281
|
+
`external: true` 让你能分辨。
|
|
282
|
+
|
|
283
|
+
### `cdp` —— 原始协议逃生舱
|
|
284
|
+
|
|
285
|
+
具名动作全都是"预先替你决定好的抽象",而 `action=cdp` 不是:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
moli_session action=cdp method=Browser.getVersion
|
|
289
|
+
moli_session action=cdp method=Network.setExtraHTTPHeaders params={"headers":{"X-Thing":"1"}}
|
|
290
|
+
moli_session action=cdp method=DOM.setFileInputFiles params={"files":["/绝对/a.png"],"nodeId":42}
|
|
241
291
|
```
|
|
242
292
|
|
|
243
|
-
|
|
244
|
-
|
|
293
|
+
它能做到包装层做不到的事:切换 target、跨域 iframe、文件上传、额外的请求头、网络拦截。`params` 可以是对象
|
|
294
|
+
或 JSON 字符串。
|
|
245
295
|
|
|
246
296
|
本工具需要 **Node 21 或更新版本**(内置 `WebSocket`);在 Node 20 上会直接说明原因,而不是给出费解的失败。
|
|
247
297
|
`moli_fetch` 与 `moli_probe` 仍支持 20.12。
|
|
@@ -324,6 +374,8 @@ cookie 驱动的查询参数。那次实测同时覆盖了代理下载路由与
|
|
|
324
374
|
maxChars: 200000 # 内联文本截断阈值
|
|
325
375
|
proxy: 'http://127.0.0.1:7890' # 转给本地正向代理(moli、下载、安装器内部引擎下载都走它)
|
|
326
376
|
insecureTls: true # 见下方 TLS 说明
|
|
377
|
+
sessionBrowser: moli # 或 chromium,供 session 工具使用
|
|
378
|
+
sessionBrowserPath: '' # 显式指定 chrome.exe / msedge.exe / brave
|
|
327
379
|
sessionPort: 9222 # moli_session 的 CDP 端口
|
|
328
380
|
blockPrivateNetworks: false # 拒绝私网地址(见下方说明)
|
|
329
381
|
blockCidrs: [] # 额外拒绝的 CIDR
|
|
@@ -387,7 +439,14 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
|
|
|
387
439
|
- `session-verify.mjs` 用 `data:` URL 夹具端到端驱动 `moli_session`,因此不依赖网络、也不会因为站点变化而
|
|
388
440
|
失效:生命周期与端口释放、snapshot、click、带与不带清空的 type、press、scroll、两个方向的 `waitFor`、
|
|
389
441
|
含抛异常的 eval、截图字节签名、点击链接导航、关闭后重启,以及上面那条视口限制——限制被当作**限制**断言,
|
|
390
|
-
紧随其后断言真正能绕过它的 `eval`
|
|
442
|
+
紧随其后断言真正能绕过它的 `eval` 路径。未安装引擎时跳过。
|
|
443
|
+
- `session-backends.mjs` 是**证明第二个后端存在必要性**的对照:同一页面、同一个视口以下的控件,断言它
|
|
444
|
+
**在 moli 上被拒绝、在 Chromium 上被点中**,并在两者上都验证 `cdp` 动作。机器上没有 Chromium 系浏览器时
|
|
445
|
+
跳过 Chromium 那半。
|
|
446
|
+
- `session-attach-verify.mjs` 覆盖外部端点与归属权:连接不是本插件启动的浏览器、正确识别它、`close` 时拒绝
|
|
447
|
+
停掉它;以及那个**行为探测会搞错的陷阱**——外部端点是 **moli** 时仍判定为 moli,尽管它在 `/json/version`
|
|
448
|
+
里报的是 `"Browser":"Chrome/145.0.0.0"`。它还断言按调用传入的端点**不会粘住**——早期版本会:一次对死端点
|
|
449
|
+
的调用会让整个进程后续都指向 9 端口。
|
|
391
450
|
|
|
392
451
|
`.github/workflows/verify.yml` 在 Linux、Windows、macOS 上跑前两个(无需引擎、无需网络),在 Linux 与 Windows
|
|
393
452
|
上尝试依赖引擎的几套;若 runner 上没有引擎,则报告为跳过而不是失败。
|
package/index.js
CHANGED
|
@@ -229,7 +229,7 @@ function renderProbe(_args, value) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
/** Actions `moli_session` accepts. */
|
|
232
|
-
const SESSION_ACTIONS = ['start', 'open', 'snapshot', 'click', 'type', 'press', 'scroll', 'waitFor', 'eval', 'screenshot', 'status', 'close'];
|
|
232
|
+
const SESSION_ACTIONS = ['start', 'open', 'snapshot', 'click', 'type', 'press', 'scroll', 'waitFor', 'eval', 'cdp', 'screenshot', 'status', 'close'];
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
235
|
* Parameters of `moli_session`.
|
|
@@ -248,7 +248,8 @@ const SESSION_PARAMETERS = {
|
|
|
248
248
|
enum: SESSION_ACTIONS,
|
|
249
249
|
description: 'start: ensure the browser is running. open: navigate. snapshot: read the rendered page. '
|
|
250
250
|
+ 'click/type/press/scroll: interact. waitFor: wait for a selector to appear or disappear. '
|
|
251
|
-
+ 'eval: run JavaScript in the page.
|
|
251
|
+
+ 'eval: run JavaScript in the page. cdp: send a raw Chrome DevTools Protocol command. '
|
|
252
|
+
+ 'screenshot: write a PNG. status: report the session and which browser it uses. close: stop the browser.',
|
|
252
253
|
},
|
|
253
254
|
url: { type: 'string', description: 'open: absolute http:// or https:// URL to navigate to.' },
|
|
254
255
|
selector: { type: 'string', description: 'CSS selector for click, type, scroll and waitFor.' },
|
|
@@ -260,12 +261,18 @@ const SESSION_PARAMETERS = {
|
|
|
260
261
|
block: { type: 'string', enum: ['start', 'center', 'end', 'nearest'], description: 'scroll: where to align the element. Defaults to center.' },
|
|
261
262
|
state: { type: 'string', enum: ['attached', 'detached'], description: 'waitFor: wait for the selector to appear (attached) or vanish (detached).' },
|
|
262
263
|
expression: { type: 'string', description: 'eval: a JavaScript expression; promises are awaited and the value is returned.' },
|
|
264
|
+
method: { type: 'string', description: 'cdp: the protocol method to invoke, e.g. "Network.enable" or "DOM.setFileInputFiles".' },
|
|
265
|
+
params: { description: 'cdp: the method parameters, as an object or a JSON string. Optional.' },
|
|
263
266
|
writeTo: { type: 'string', description: 'screenshot: absolute path for the PNG. Required, since images are not returned inline.' },
|
|
264
267
|
format: { type: 'string', enum: ['png', 'jpeg'], description: 'screenshot: image format. Defaults to png.' },
|
|
265
268
|
includeHtml: { type: 'boolean', description: 'snapshot: also return the rendered HTML.' },
|
|
266
|
-
maxChars: { type: 'integer', description: 'snapshot: truncate
|
|
269
|
+
maxChars: { type: 'integer', description: 'snapshot and cdp: truncate text output to this many characters.' },
|
|
267
270
|
timeoutMs: { type: 'integer', description: 'Deadline for this action. Defaults to the plugin configuration.' },
|
|
268
271
|
port: { type: 'integer', description: 'start: CDP port. Defaults to the plugin configuration (9222).' },
|
|
272
|
+
cdpEndpoint: {
|
|
273
|
+
type: 'string',
|
|
274
|
+
description: 'Attach to a browser that is already running, e.g. "http://127.0.0.1:9222", instead of starting one. This is how you drive your own Chrome with its logins: start it with --remote-debugging-port and pass that URL.',
|
|
275
|
+
},
|
|
269
276
|
},
|
|
270
277
|
};
|
|
271
278
|
|
|
@@ -301,6 +308,12 @@ const SESSION_OUTPUT_SCHEMA = {
|
|
|
301
308
|
path: { type: 'string' },
|
|
302
309
|
bytes: { type: 'integer' },
|
|
303
310
|
format: { type: 'string' },
|
|
311
|
+
method: { type: 'string' },
|
|
312
|
+
result: {},
|
|
313
|
+
bytes: { type: 'integer' },
|
|
314
|
+
backend: { type: 'string' },
|
|
315
|
+
geometry: { type: 'string' },
|
|
316
|
+
browserVersion: { type: 'string' },
|
|
304
317
|
running: { type: 'boolean' },
|
|
305
318
|
ownedByThisPlugin: { type: 'boolean' },
|
|
306
319
|
external: { type: 'boolean' },
|
|
@@ -310,6 +323,7 @@ const SESSION_OUTPUT_SCHEMA = {
|
|
|
310
323
|
settled: { type: 'boolean' },
|
|
311
324
|
settledMs: { type: 'integer' },
|
|
312
325
|
stopped: { type: 'boolean' },
|
|
326
|
+
scope: { type: 'string' },
|
|
313
327
|
},
|
|
314
328
|
};
|
|
315
329
|
|
|
@@ -344,12 +358,21 @@ function renderSession(_args, value) {
|
|
|
344
358
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
345
359
|
}
|
|
346
360
|
if (value.action === 'status') {
|
|
347
|
-
|
|
348
|
-
type: 'text',
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
361
|
+
if (value.running !== true) {
|
|
362
|
+
return [{ type: 'text', text: 'No session is running. Call moli_session with action "start" or "open" to launch one.' }];
|
|
363
|
+
}
|
|
364
|
+
const lines = [
|
|
365
|
+
`Session running: ${String(value.backend)} (${String(value.browserVersion ?? 'version unknown')}) at ${String(value.endpoint)}`,
|
|
366
|
+
];
|
|
367
|
+
if (value.geometry === 'absolute') {
|
|
368
|
+
lines.push('Geometry: moli\'s document-absolute frame — element input cannot reach below the first viewport. Use `browser: "chromium"` to lift that, or eval to click in the page.');
|
|
369
|
+
}
|
|
370
|
+
if (value.external === true) lines.push('Started outside this plugin, so close will not stop it.');
|
|
371
|
+
if (value.url !== undefined) lines.push(`Current page: ${String(value.title)} — ${String(value.url)}`);
|
|
372
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
373
|
+
}
|
|
374
|
+
if (value.action === 'cdp') {
|
|
375
|
+
return [{ type: 'text', text: `${String(value.method)} → ${String(value.serialized ?? '').slice(0, 4000)}` }];
|
|
353
376
|
}
|
|
354
377
|
if (value.action === 'click') {
|
|
355
378
|
return [{
|
|
@@ -513,6 +536,13 @@ export function apply(ctx, config) {
|
|
|
513
536
|
// load time, so a plugin load cannot delay Host startup or open a port.
|
|
514
537
|
const session = new SessionManager({
|
|
515
538
|
get exe() { return cachedExe; },
|
|
539
|
+
// Which browser drives a session. Explicit, never automatic: switching engines
|
|
540
|
+
// because one click failed would make the timing and the behaviour depend on
|
|
541
|
+
// which operation happened to fail, and the caller could not tell which browser
|
|
542
|
+
// it was talking to.
|
|
543
|
+
browser: options.sessionBrowser ?? 'moli',
|
|
544
|
+
browserPath: options.sessionBrowserPath,
|
|
545
|
+
cdpEndpoint: options.sessionCdpEndpoint,
|
|
516
546
|
port: options.sessionPort ?? 9222,
|
|
517
547
|
host: '127.0.0.1',
|
|
518
548
|
insecureTls,
|
|
@@ -544,29 +574,40 @@ export function apply(ctx, config) {
|
|
|
544
574
|
// navigate the page out from under each other.
|
|
545
575
|
isConcurrencySafe: () => false,
|
|
546
576
|
async execute(args, exec) {
|
|
577
|
+
// A per-call endpoint is honoured by every action, including the ones that
|
|
578
|
+
// never reach `performAction`. Routing `close` through it is not cosmetic:
|
|
579
|
+
// `withEndpoint` is what marks the connection external, and `close` refuses
|
|
580
|
+
// to stop an external browser. Answering `close` before that mark existed
|
|
581
|
+
// meant a call that had just attached to the user's own Chrome could shut it
|
|
582
|
+
// down -- the one thing this design promises not to do.
|
|
583
|
+
const withCallEndpoint = work => (typeof args.cdpEndpoint === 'string' && args.cdpEndpoint !== ''
|
|
584
|
+
? session.withEndpoint(args.cdpEndpoint, work)
|
|
585
|
+
: work());
|
|
586
|
+
|
|
547
587
|
// `status` and `close` are meaningful with nothing running, so they are
|
|
548
588
|
// answered before the engine is required.
|
|
549
589
|
if (args.action === 'close') {
|
|
550
|
-
|
|
551
|
-
return { ok: true, action: 'close', ...outcome };
|
|
590
|
+
return withCallEndpoint(async () => ({ ok: true, action: 'close', ...(await session.close()) }));
|
|
552
591
|
}
|
|
553
592
|
if (args.action === 'status') {
|
|
554
|
-
|
|
555
|
-
return { ok: true, action: 'status', ...state };
|
|
593
|
+
return withCallEndpoint(async () => ({ ok: true, action: 'status', ...(await session.status()) }));
|
|
556
594
|
}
|
|
557
595
|
if (args.action === 'start') {
|
|
558
596
|
if (!sessionSupported()) return { ok: false, action: 'start', error: sessionUnsupportedReason() };
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
597
|
+
return withCallEndpoint(async () => {
|
|
598
|
+
const { external, backend, geometry } = await session.ensure();
|
|
599
|
+
const state = await session.status();
|
|
600
|
+
return { ok: true, action: 'start', started: true, external, backend, geometry, ...state };
|
|
601
|
+
});
|
|
562
602
|
}
|
|
563
603
|
|
|
564
|
-
if (engine() === undefined)
|
|
604
|
+
if (engine() === undefined && (options.sessionBrowser ?? 'moli') === 'moli'
|
|
605
|
+
&& (options.sessionCdpEndpoint === undefined || options.sessionCdpEndpoint === '')) {
|
|
565
606
|
return {
|
|
566
607
|
ok: false,
|
|
567
608
|
action: String(args.action),
|
|
568
|
-
error: 'moli was not found, so a session cannot be started',
|
|
569
|
-
hint: 'Call moli_probe with action "install",
|
|
609
|
+
error: 'moli was not found, so a moli session cannot be started',
|
|
610
|
+
hint: 'Call moli_probe with action "install", set the plugin config field `exePath`, or set `sessionBrowser: "chromium"` to drive the browser you already have.',
|
|
570
611
|
};
|
|
571
612
|
}
|
|
572
613
|
if (!sessionSupported()) return { ok: false, action: String(args.action), error: sessionUnsupportedReason() };
|
|
@@ -574,23 +615,43 @@ export function apply(ctx, config) {
|
|
|
574
615
|
if (exec?.signal?.aborted === true) return { ok: false, action: String(args.action), error: 'the call was aborted before it started' };
|
|
575
616
|
|
|
576
617
|
try {
|
|
577
|
-
const { connection }
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
618
|
+
const perform = async ({ connection, geometry, backend }) => {
|
|
619
|
+
try {
|
|
620
|
+
return await performAction(connection, args, { maxChars, geometry, backend });
|
|
621
|
+
} finally {
|
|
622
|
+
// A closed socket means the browser went away under us; drop it so the
|
|
623
|
+
// next call starts a fresh one instead of failing forever.
|
|
624
|
+
if (connection.closed === true) void session.close();
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
// A per-call endpoint drives a browser the caller already has open, which is
|
|
628
|
+
// the only way to inherit their logins. It is scoped to this call: an
|
|
629
|
+
// endpoint that latched would make one bad call poison every later one.
|
|
630
|
+
if (typeof args.cdpEndpoint === 'string' && args.cdpEndpoint !== '') {
|
|
631
|
+
return await session.withEndpoint(args.cdpEndpoint, perform);
|
|
632
|
+
}
|
|
633
|
+
return await perform(await session.ensure());
|
|
583
634
|
} catch (error) {
|
|
584
635
|
const message = error instanceof Error ? error.message : String(error);
|
|
585
636
|
const hint = /timed out/u.test(message)
|
|
586
637
|
? 'The action exceeded its deadline. Raise timeoutMs, or check `waitFor` for a selector that never appears.'
|
|
587
|
-
: /
|
|
588
|
-
? '
|
|
589
|
-
:
|
|
638
|
+
: /no Chromium-family browser/u.test(message)
|
|
639
|
+
? 'Install Chrome, Edge or Brave, or set the plugin config field `sessionBrowserPath` to a browser executable.'
|
|
640
|
+
: /ECONNREFUSED|closed|WebSocket/u.test(message)
|
|
641
|
+
? 'The browser connection was lost. Call moli_session with action "status", then "start" to relaunch it.'
|
|
642
|
+
: undefined;
|
|
590
643
|
return { ok: false, action: String(args.action), error: message, ...(hint === undefined ? {} : { hint }) };
|
|
591
644
|
}
|
|
592
645
|
},
|
|
593
646
|
});
|
|
594
647
|
|
|
648
|
+
// A serve process or a headless browser outlives a tool call, so it must be tied
|
|
649
|
+
// to the plugin's lifetime rather than left to the OS. Cordis disposes listeners
|
|
650
|
+
// registered through `ctx.on` together with the plugin's fiber, so registering
|
|
651
|
+
// one is enough; the returned disposer is not needed here.
|
|
652
|
+
if (typeof ctx.on === 'function') {
|
|
653
|
+
ctx.on('dispose', () => { void session.close(); });
|
|
654
|
+
}
|
|
655
|
+
|
|
595
656
|
ctx.logger?.info?.(`dsh-moli: registered moli_fetch, moli_probe and moli_session (${cachedExe ?? 'moli missing'})`);
|
|
596
657
|
}
|