dsh-moli 1.1.15 → 1.1.16
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 +25 -2
- package/README.zh.md +24 -5
- package/index.js +7 -0
- package/lib/moli.js +79 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -242,8 +242,29 @@ Measured on a page whose `scroll` handler fills a slot: without `scroll` the dum
|
|
|
242
242
|
`STILL-EMPTY`, with `scroll=1000` it says `LAZY-CONTENT-1000`. It also moves the screenshot, so a
|
|
243
243
|
scrolled `dump=screenshot` shows that part of the page.
|
|
244
244
|
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
`scroll` is a side effect, so it composes and stands alone:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
moli_fetch url=... dump=markdown scroll=600 # scroll, then dump the page
|
|
249
|
+
moli_fetch url=... eval="document.title" scroll=600 # scroll, then read a value
|
|
250
|
+
moli_fetch url=... scroll=600 # just scroll; you still get the page
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The last form is worth spelling out because it was broken. The scroll runs in the engine's own
|
|
254
|
+
pre-capture evaluation, and without `dump` the engine prints whatever that evaluation returns — so an
|
|
255
|
+
earlier version that returned the scroll position made this call answer `600` instead of the document,
|
|
256
|
+
while the identical call without `scroll` returned 1,989 characters of HTML. A scroll reports nothing
|
|
257
|
+
now; it only happens.
|
|
258
|
+
|
|
259
|
+
### `eval` with `dump` — the expression value is dropped
|
|
260
|
+
|
|
261
|
+
The engine documents this and it is kept, because using an expression to change a page before
|
|
262
|
+
capturing it is a real pattern. What is not acceptable is that it used to look like a clean answer:
|
|
263
|
+
`eval='JSON.stringify({probe:1})'` with `dump=markdown` returned 900 characters of Markdown, reported
|
|
264
|
+
`ok: true`, and said nothing. The result now carries `evalDiscarded: true` and a note, and the rendered
|
|
265
|
+
output leads with the warning rather than burying it under the body.
|
|
266
|
+
|
|
267
|
+
If you want the expression's value, pass `eval` without `dump`.
|
|
247
268
|
|
|
248
269
|
### `failOnHttpError` — stop reading an error page as content
|
|
249
270
|
|
|
@@ -472,6 +493,8 @@ Eight suites live in [`verify/`](verify/), and all are runnable against a workin
|
|
|
472
493
|
reaches the engine is indistinguishable from a feature that does not work. It then fetches a 404 and
|
|
473
494
|
checks that `failOnHttpError` reports it while keeping the body, that a healthy page is unaffected,
|
|
474
495
|
and that a screenshot of an error page is still written as a valid PNG rather than decoded as text.
|
|
496
|
+
It also covers the two silent-answer cases: `eval` with `dump` is flagged `evalDiscarded` and the
|
|
497
|
+
warning renders first, and `scroll` alone returns the document rather than the scroll offset.
|
|
475
498
|
The end-to-end half needs an HTTP error from a real server: loopback is unreachable from inside this
|
|
476
499
|
sandbox — `curl.exe` times out on `127.0.0.1` while the Node host reaches it — so it uses a path
|
|
477
500
|
under `example.com` and skips when the network is away.
|
package/README.zh.md
CHANGED
|
@@ -227,8 +227,26 @@ moli_fetch url=https://example.com/feed dump=markdown scroll=-400 # 往回滚
|
|
|
227
227
|
`scroll=1000` 时是 `LAZY-CONTENT-1000`。它同样会改变截图,所以带滚动的 `dump=screenshot` 拍到的是页面的
|
|
228
228
|
那一段。
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
`scroll` 是副作用,因此既可组合也可单独使用:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
moli_fetch url=... dump=markdown scroll=600 # 先滚动,再 dump 页面
|
|
234
|
+
moli_fetch url=... eval="document.title" scroll=600 # 先滚动,再取一个值
|
|
235
|
+
moli_fetch url=... scroll=600 # 只滚动,你仍然拿到页面
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**最后一种写法值得单独说明,因为它曾经是坏的。** 滚动在引擎自身的"抓取前求值"里执行,而在没有 `dump` 时,
|
|
239
|
+
引擎打印的就是这个求值的返回值——所以早期版本返回滚动位置,导致这一调用回的是 `600` 而不是页面,而**同样的
|
|
240
|
+
调用不加 `scroll` 会返回 1989 个字符的 HTML**。现在滚动不报告任何东西,它只是发生。
|
|
241
|
+
|
|
242
|
+
### `eval` 与 `dump` 同用 —— 表达式值会被丢弃
|
|
243
|
+
|
|
244
|
+
这是引擎写明并有意的行为,予以保留——**用表达式在抓取前改变页面**是真实用法。不能接受的是它**看起来像一个
|
|
245
|
+
干净答案**:`eval='JSON.stringify({probe:1})'` 配 `dump=markdown` 会返回 900 个字符的 Markdown、报
|
|
246
|
+
`ok: true`、什么都不说。现在结果会带 `evalDiscarded: true` 和说明,而且渲染输出**把警告放在最前面**,不会
|
|
247
|
+
被正文埋掉。
|
|
248
|
+
|
|
249
|
+
要拿表达式的值,就只传 `eval`、不要传 `dump`。
|
|
232
250
|
|
|
233
251
|
### `failOnHttpError` —— 别再把错误页当正文读
|
|
234
252
|
|
|
@@ -428,9 +446,10 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
|
|
|
428
446
|
断言的是**输出**的性质,而不是内部调用。
|
|
429
447
|
- `http-verify.mjs` 直接断言"参数 → 引擎选项"的映射,因为**参数没送到引擎和功能不生效,从外面看是一样的**。
|
|
430
448
|
随后抓一个 404,验证 `failOnHttpError` 会报告失败同时保留响应体、健康页面不受影响、以及错误页的截图仍然被
|
|
431
|
-
写为合法 PNG
|
|
432
|
-
|
|
433
|
-
|
|
449
|
+
写为合法 PNG 而不是被当文本解码。它还覆盖那两个"静默给错答案"的情形:`eval` 配 `dump` 会带
|
|
450
|
+
`evalDiscarded` 标记且警告渲染在最前,`scroll` 单独用返回的是文档而不是滚动偏移。端到端部分需要一个真实
|
|
451
|
+
服务器的 HTTP 错误:**本沙箱内无法访问 loopback**——`curl.exe` 连 `127.0.0.1` 会超时,而 Node 宿主进程
|
|
452
|
+
可以——所以它用 `example.com` 下的一个路径,网络不可用时跳过。
|
|
434
453
|
|
|
435
454
|
### 测试前置条件
|
|
436
455
|
|
package/index.js
CHANGED
|
@@ -174,6 +174,7 @@ const FETCH_OUTPUT_SCHEMA = {
|
|
|
174
174
|
empty: { type: 'boolean' },
|
|
175
175
|
omitted: { type: 'integer' },
|
|
176
176
|
evalFailed: { type: 'boolean' },
|
|
177
|
+
evalDiscarded: { type: 'boolean' },
|
|
177
178
|
truncated: { type: 'boolean' },
|
|
178
179
|
error: { type: 'string' },
|
|
179
180
|
hint: { type: 'string' },
|
|
@@ -243,6 +244,12 @@ function renderFetch(_args, value) {
|
|
|
243
244
|
}];
|
|
244
245
|
}
|
|
245
246
|
const parts = [];
|
|
247
|
+
// A discarded `eval` value is the one case where the output looks like a clean
|
|
248
|
+
// answer to a question that was never answered, so the warning goes first rather
|
|
249
|
+
// than after the body, where a 900-character page would bury it.
|
|
250
|
+
if (value.evalDiscarded === true) {
|
|
251
|
+
parts.push('Note: the `eval` expression was dropped because `dump` was given too — the engine discards the expression value in that combination. What follows is the page, not the expression.');
|
|
252
|
+
}
|
|
246
253
|
if (typeof value.note === 'string' && value.note !== '') parts.push(value.note);
|
|
247
254
|
parts.push(String(value.content ?? ''));
|
|
248
255
|
return [{ type: 'text', text: parts.join('\n\n') }];
|
package/lib/moli.js
CHANGED
|
@@ -56,6 +56,14 @@ const SCROLL_TOKENS = {
|
|
|
56
56
|
* content that "below the fold" already contains; it is about triggering the
|
|
57
57
|
* handler, the intersection observer, or the lazy image that only runs on a scroll.
|
|
58
58
|
*
|
|
59
|
+
* **The expression must not return the scroll position.** Without `--dump` the
|
|
60
|
+
* engine prints whatever `--eval` evaluates to, so returning `String(window.scrollY)`
|
|
61
|
+
* here replaced the page with the number `600`: `moli_fetch url=... scroll=600`
|
|
62
|
+
* answered `600`, while the identical call without `scroll` returned the document's
|
|
63
|
+
* HTML. The scroll is a side effect; the value it wants to report is recorded on
|
|
64
|
+
* `window` instead, where a later read can pick it up without changing what the
|
|
65
|
+
* engine prints.
|
|
66
|
+
*
|
|
59
67
|
* @param scroll - pixels down (negative scrolls up), or `top` / `bottom`.
|
|
60
68
|
* @returns a JavaScript expression, or undefined when there is nothing to do.
|
|
61
69
|
*/
|
|
@@ -74,26 +82,55 @@ export function scrollScript(scroll) {
|
|
|
74
82
|
return undefined;
|
|
75
83
|
}
|
|
76
84
|
// Dispatch a scroll event as well as moving: `window.scrollTo` updates the
|
|
77
|
-
// position, but a page listening for the event is the reason this exists.
|
|
78
|
-
|
|
85
|
+
// position, but a page listening for the event is the reason this exists. The
|
|
86
|
+
// height is captured on both sides of the move so a caller can tell whether the
|
|
87
|
+
// page grew -- which is the one signal that says lazy loading actually happened.
|
|
88
|
+
return `(() => {
|
|
89
|
+
const before = document.body === null ? 0 : document.body.scrollHeight;
|
|
90
|
+
window.scrollTo(0, ${target});
|
|
91
|
+
window.dispatchEvent(new Event('scroll'));
|
|
92
|
+
window.__dshMoliScroll = {
|
|
93
|
+
requested: ${target},
|
|
94
|
+
y: window.scrollY,
|
|
95
|
+
heightBefore: before,
|
|
96
|
+
heightAfter: document.body === null ? 0 : document.body.scrollHeight,
|
|
97
|
+
};
|
|
98
|
+
})()`;
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
/**
|
|
82
102
|
* Combine the caller's `eval` with a scroll, if one was asked for.
|
|
83
103
|
*
|
|
84
|
-
* The two
|
|
85
|
-
*
|
|
86
|
-
*
|
|
104
|
+
* The two share one expression because the engine accepts a single `--eval`.
|
|
105
|
+
*
|
|
106
|
+
* What gets returned depends on whether anything else will produce the output,
|
|
107
|
+
* and getting this wrong breaks the feature in one direction or the other:
|
|
87
108
|
*
|
|
88
|
-
*
|
|
109
|
+
* - **A `dump` was requested.** The dump produces the output, so the expression is
|
|
110
|
+
* there only to scroll first and its value is discarded by the engine anyway.
|
|
111
|
+
* It must still be passed, or the scroll never runs.
|
|
112
|
+
* - **No `dump`, and the caller gave an expression.** The expression value *is* the
|
|
113
|
+
* output, and the scroll runs as part of it.
|
|
114
|
+
* - **No `dump`, no caller expression.** The engine prints whatever `--eval`
|
|
115
|
+
* evaluates to, so passing the scroll expression here would replace the page with
|
|
116
|
+
* the scroll's return value. `undefined` is returned instead, leaving the engine's
|
|
117
|
+
* default output -- the serialized document -- intact. This is the case that
|
|
118
|
+
* shipped broken: `moli_fetch url=... scroll=600` answered `600`.
|
|
119
|
+
*
|
|
120
|
+
* @param evalExpression - the caller's expression, if any.
|
|
121
|
+
* @param scroll - the caller's `scroll` value, if any.
|
|
122
|
+
* @param hasDump - whether the caller also asked for a dump.
|
|
123
|
+
* @returns the expression to pass, or undefined to let the engine decide.
|
|
89
124
|
*/
|
|
90
|
-
export function combineEval(evalExpression, scroll) {
|
|
125
|
+
export function combineEval(evalExpression, scroll, hasDump = false) {
|
|
91
126
|
const move = scrollScript(scroll);
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
// The caller's expression
|
|
95
|
-
// engine reports
|
|
96
|
-
return `${move}; (() => (${
|
|
127
|
+
const caller = evalExpression === undefined || evalExpression === '' ? undefined : evalExpression;
|
|
128
|
+
if (move === undefined) return caller;
|
|
129
|
+
// The caller's expression goes last and is wrapped so its value is the one the
|
|
130
|
+
// engine reports; losing it would contradict the documented `eval` behaviour.
|
|
131
|
+
if (caller !== undefined) return `${move}; (() => (${caller}))()`;
|
|
132
|
+
// No caller expression: keep the scroll only when a dump will produce the output.
|
|
133
|
+
return hasDump ? move : undefined;
|
|
97
134
|
}
|
|
98
135
|
|
|
99
136
|
/**
|
|
@@ -334,7 +371,11 @@ export function buildArgs(args) {
|
|
|
334
371
|
if (args.scrollbars === true) argv.push('--scrollbars');
|
|
335
372
|
// `scroll` rides along in the same expression: the engine takes a single `--eval`,
|
|
336
373
|
// and the scroll has to happen before the page is captured.
|
|
337
|
-
|
|
374
|
+
// `scroll` rides along in the same expression: the engine takes a single `--eval`,
|
|
375
|
+
// and the scroll has to happen before the page is captured. `hasDump` matters --
|
|
376
|
+
// see combineEval -- because with a dump the expression exists only for its side
|
|
377
|
+
// effect, and without one it would become the output.
|
|
378
|
+
const evaluation = combineEval(args.eval, args.scroll, args.dump !== undefined);
|
|
338
379
|
if (evaluation !== undefined) argv.push('--eval', evaluation);
|
|
339
380
|
if (args.wait !== undefined && args.wait !== '') argv.push('--wait', args.wait);
|
|
340
381
|
if (args.waitSelector !== undefined && args.waitSelector !== '') argv.push('--wait-selector', args.waitSelector);
|
|
@@ -561,6 +602,16 @@ export function runFetch(options) {
|
|
|
561
602
|
}
|
|
562
603
|
|
|
563
604
|
const text = outcome.stdout.toString('utf8');
|
|
605
|
+
// `eval` together with `dump` is the engine's documented behaviour -- it discards
|
|
606
|
+
// the expression's value and dumps the page state the expression produced -- but
|
|
607
|
+
// it reads as a successful answer to a question that was never answered. The
|
|
608
|
+
// caller asked for `{probe:1}` and got 900 characters of Markdown. The discard is
|
|
609
|
+
// kept, because mutating a page before capturing it is a real use, so the result
|
|
610
|
+
// has to say what happened instead of looking clean.
|
|
611
|
+
const evalDiscarded = args.eval !== undefined && args.eval !== '' && args.dump !== undefined;
|
|
612
|
+
const evalNote = evalDiscarded
|
|
613
|
+
? 'The `eval` expression produced no value here: `dump` was given too, and the engine discards the expression result in that combination. The output above is the page, not the expression. Run again with `eval` alone for the value, or keep both deliberately when the expression only changes the page before it is captured.'
|
|
614
|
+
: undefined;
|
|
564
615
|
// A JSON dump is the only format that reports the HTTP status, so an error
|
|
565
616
|
// page is otherwise indistinguishable from content. Surface it rather than
|
|
566
617
|
// letting the model read a 404 body as if the fetch succeeded.
|
|
@@ -591,7 +642,10 @@ export function runFetch(options) {
|
|
|
591
642
|
path: args.writeTo,
|
|
592
643
|
chars: text.length,
|
|
593
644
|
bytes,
|
|
594
|
-
note:
|
|
645
|
+
note: evalNote === undefined
|
|
646
|
+
? 'Full output saved to file; read it for the complete text.'
|
|
647
|
+
: `Full output saved to file; read it for the complete text. ${evalNote}`,
|
|
648
|
+
...(evalDiscarded ? { evalDiscarded: true } : {}),
|
|
595
649
|
};
|
|
596
650
|
}
|
|
597
651
|
|
|
@@ -617,6 +671,12 @@ export function runFetch(options) {
|
|
|
617
671
|
content = text.slice(0, headLen) + mark + text.slice(text.length - tailLen);
|
|
618
672
|
note = `Output truncated: showing the first ${String(headLen)} and the last ${String(tailLen)} of ${String(text.length)} characters, with ${String(omitted)} omitted in the middle. Pass writeTo to keep all ${String(text.length)}.`;
|
|
619
673
|
}
|
|
674
|
+
// `eval` together with `dump` is the engine's documented behaviour -- it discards
|
|
675
|
+
// the expression's value and dumps the page state the expression produced -- but
|
|
676
|
+
// it reads as a successful answer to a question that was never answered. The
|
|
677
|
+
// caller asked for `{probe:1}` and got 900 characters of Markdown. The value is
|
|
678
|
+
// still discarded (that combination is genuinely useful for mutating a page before
|
|
679
|
+
// capturing it), so the result has to say so rather than look clean.
|
|
620
680
|
return {
|
|
621
681
|
ok: true,
|
|
622
682
|
url: args.url,
|
|
@@ -626,7 +686,11 @@ export function runFetch(options) {
|
|
|
626
686
|
chars: text.length,
|
|
627
687
|
bytes,
|
|
628
688
|
truncated,
|
|
629
|
-
...(truncated ? { omitted
|
|
689
|
+
...(truncated ? { omitted } : {}),
|
|
690
|
+
...(evalDiscarded ? { evalDiscarded: true } : {}),
|
|
691
|
+
...(note === undefined && evalNote === undefined
|
|
692
|
+
? {}
|
|
693
|
+
: { note: [note, evalNote].filter(part => part !== undefined).join(' ') }),
|
|
630
694
|
};
|
|
631
695
|
}
|
|
632
696
|
|
package/package.json
CHANGED