dsh-read-url 0.4.8 → 1.0.0
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/CHANGELOG.md +73 -0
- package/README.md +59 -25
- package/README.zh.md +61 -28
- package/cordis.patch.yml +2 -0
- package/index.js +507 -65
- package/package.json +2 -2
- package/proxy-fallback.js +25 -1
- package/spa.js +41 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0] - 2026-08-21
|
|
4
|
+
|
|
5
|
+
### 量大管饱的持久版本:一次把「读网页」这件事做全
|
|
6
|
+
|
|
7
|
+
目标:省 token 不变的前提下,覆盖绝大多数 URL 类型(HTML / JSON / RSS / Atom / 多页长文 / 各国编码),把后续更新频率降到最低。
|
|
8
|
+
|
|
9
|
+
**新增:内容类型分发(HTML 不再是唯一入口)**
|
|
10
|
+
- JSON 数据 API(如 `api.github.com`)→ 原生紧凑渲染(`JSON.stringify` 缩进 1 格),不再报 "Unsupported content-type"
|
|
11
|
+
- RSS 2.0 / Atom 订阅源 → 解析为紧凑条目列表(`标题 — 链接 + 摘要`),返回 `feedCount`,`includeLinks` 时附完整 items;XML sitemap 仍明确拒绝(对模型无阅读价值)
|
|
12
|
+
- 修复 proxy-fallback 的 content-type 正则:`application/rss+xml` 分隔符是 `+` 不是 `/`,此前被误拦
|
|
13
|
+
- web seam 路径同样分流:seam 返回体以 `{`/`[` 开头时尝试 JSON 渲染
|
|
14
|
+
|
|
15
|
+
**新增:分页长文自动拼接(默认 3 页)**
|
|
16
|
+
- 识别 `rel=next`(标准)或纯「下一页 / 下页 / 后一页 / 下一頁 / next / › / » / older entries」短锚文本——刻意保守,不做模糊猜测
|
|
17
|
+
- 同域限定 + 已见 URL 集合防环;续页走静态快路径(分页 SPA 链每页一次完整渲染不划算);`joinPageText` 去除跨页重复段落/标题(30–300 字符重叠窗口)
|
|
18
|
+
- 输出新增 `paginated` 字段,正文标注 `(已自动拼接 N 页)`;`paginate: false` 关闭,`paginateMax` 可调(1–10)
|
|
19
|
+
- 工具超时预算随之修正:`timeoutMs × (paginateMax + 1) + 45s`(首页 + SPA 渲染 + 续页抓取),随配置缩放
|
|
20
|
+
|
|
21
|
+
**新增:文本密度兜底(无标准容器页)**
|
|
22
|
+
- 无 `<article>`/`<main>` 而退化到整页 body 的页面:按块级标签切块,丢弃「短且链接主导」(正文 < 300 字符且锚文本占比 > 65%)的相关推荐 / 分类侧栏 / 热门文章挂件
|
|
23
|
+
- 标准容器页面完全不走此路径,零行为变化
|
|
24
|
+
|
|
25
|
+
**新增:编码增强**
|
|
26
|
+
- BOM 优先探测:UTF-8 / UTF-16LE / UTF-16BE(字节级证据优先于任何声明——UTF-16 body 用 latin1 读 `<meta>` 探针本来就匹配不上)
|
|
27
|
+
- Shift-JIS 等日韩编码:页面声明 charset 即可由内置 TextDecoder 正确解码(实测日文页面全绿)
|
|
28
|
+
|
|
29
|
+
**新增:网络韧性**
|
|
30
|
+
- 429/503 感知 `Retry-After`(秒形式,封顶 5s 保证协作超时仍约束调用)重试一次
|
|
31
|
+
- 空响应(204/304)沿用 v0.4.9 防护
|
|
32
|
+
|
|
33
|
+
**新增:Markdown / 元数据增强**
|
|
34
|
+
- `<img>` 带 alt+src → ``(哨兵占位法穿过转义层;空 alt 装饰图丢弃——模型反正看不见像素)
|
|
35
|
+
- 代码块围栏带语言标注(识别 `language-*` / `lang-*` class,```js 远比无标注准确)
|
|
36
|
+
- 页级元数据提取:`published`(article:published_time / og:published_time / pubdate / dc.date 等)、`author`(author / og:article:author 等)进输出字段与状态行
|
|
37
|
+
- 空正文页(登录墙 / JS 壳)回落到 `og:description` 作为提示,不再返回空
|
|
38
|
+
|
|
39
|
+
**测试**:42 → **60 断言**(+UTF-16 BOM、Shift-JIS、密度过滤、分页拼接/封顶/关闭、JSON 渲染、RSS 解析、sitemap 拒绝、429 重试、图片 alt、代码语言、元数据、og:description 回落)+ test-spa 10 全绿 + **47 站真实回归**(新增 ruanyifeng Atom / github-blog Atom / v2ex Atom / GitHub API JSON / 起点·开源中国分页候选站)
|
|
40
|
+
|
|
41
|
+
**发布前实测修复(152 站全量扫描 + 逐站诊断定位)**
|
|
42
|
+
- RSS 描述双重转义(`<a>` 嵌套实体)→ `xmlText` 迭代「剥标签+解实体」直到稳定,正文不再出现字面 `<a href>`
|
|
43
|
+
- 无 Content-Type 头的二进制响应(字节级嗅探:NUL 字节或控制字符占比 > 30%)被当 HTML 解码出乱码 → `looksBinary` 嗅探拒绝,直连与代理路径统一防护
|
|
44
|
+
- JS 跳转壳(script 数低于 SPA 阈值但 body 为空,实测 taptap.cn 形态)不触发渲染 → 空 body 且含任意 `<script>` 即渲染
|
|
45
|
+
- **`role="main"` 容器被非贪婪正则截断**:`<div role="main">` 内嵌套 div 时匹配到第一个 `</div>` 就停(实测 gnu.org 只取到 2700/20000+ 字符,naver.com 只取到 115 字符即 0 文本)→ `tagBlockAt` 深度计数找平衡闭合标签
|
|
46
|
+
- **小 `<article>` 劫持主内容**:页面存在无关小 article(如订阅卡片,实测 about.gitlab.com 71 字符)时优先级高于 `<main>` → article 文本 < 200 字符且页面有 `<main>` 时回落 main
|
|
47
|
+
- **渲染结果接受门槛过严**:静态提取为空时,渲染出 20–50 字符的短正文(JS 壳典型形态)被 `+50` 增量门槛拒绝 → 空静态 + 渲染 ≥ 20 字符即接受
|
|
48
|
+
|
|
49
|
+
**发布前实测修复二(DSH 15 项全功能验收驱动)**
|
|
50
|
+
- **Cloudflare 人机验证页劫持正文**:挑战过渡页("Just a moment...")文字量可超过真实静态正文(实测 259 vs 135 字符),被渲染提升逻辑误当作更优结果 → 三层防御:`looksLikeChallenge` 高特异性特征(cf-chl / cdn-cgi/challenge-platform 等)识别;renderPage 检测到挑战页时额外轮询 8s 等待自动跳转(验证常在数秒内自行通过);仍是指纹页则拒绝渲染结果、保留静态正文 + 明确提示,且挑战页 DOM 不污染分页/链接提取
|
|
51
|
+
- **元数据 byline 兜底**:无任何 author/date meta 的页面(实测阮一峰博客零 meta 标签)从正文头部 600 字符提取「作者:XXX / 日期:2026年8月21日」等署名行——仅填空缺字段、meta 优先、深部提及不误采、markdown 链接语法不泄漏
|
|
52
|
+
- 测试 PDF 样本 URL 失效(africau.edu sample.pdf 已 404)→ 换 mozilla.github.io 稳定样本,PDF 类型拒绝路径恢复验证(明确报 `Unsupported content-type: application/pdf`)
|
|
53
|
+
|
|
54
|
+
**测试(发布前终态)**:**77 断言**(+双转义 feed、无头二进制拒绝、嵌套 role=main、薄 article 回落 main、不平衡标签降级、byline 兜底四态、挑战页识别不误伤正文)+ test-spa **12 全绿**(+JS 跳转壳渲染)+ **152 站真实回归**(113 OK / 16 预期边界 THIN / 23 环境类 ERR / 0 崩溃;对比修复前 THIN 20 → 16,gnu.org 165→800 字符、gitlab 71→800 字符)+ **DSH 15 项全功能验收**(12 项完全符合,3 项问题全部修复闭环:验证页防御、byline 字段、PDF 类型归因)
|
|
55
|
+
|
|
56
|
+
**发布策略**:大版本收敛——此后以修 bug 为主,减少更新频率。
|
|
57
|
+
|
|
58
|
+
## [0.4.9] - 2026-08-20
|
|
59
|
+
|
|
60
|
+
### 稳定性:配置健壮性 + 严格宿主兼容 + 资源泄漏
|
|
61
|
+
|
|
62
|
+
**修复(真 bug)**
|
|
63
|
+
- `cordis.patch.yml` 数字加引号(`timeoutMs: "15000"`)时按字符串拼接进超时预算(`"1500020000"`)→ 加载时统一强转数字并钳制到合理范围(timeoutMs 500–120000 等),非法值回退默认
|
|
64
|
+
- 严格 cordis 宿主上 `ctx.get('web')` 对未注入服务直接抛错(同 `settings` 踩过的坑)→ 移入 try,降级为全局 fetch
|
|
65
|
+
- 超时预算公式低估最坏链路:SPA 渲染实际是 goto 30s 上限 + DOM 轮询 10s = 40s,原公式只留 20s → 修正为 `timeoutMs + 45s`,且随配置缩放(60s 配置得 105s 预算,不再钳制)
|
|
66
|
+
- `read_url_site` 预算 120s 覆盖不了 50 页 × 长超时的爬取波次 → 改为随配置缩放(`timeoutMs × 25` 波)
|
|
67
|
+
- `crawlSite` 在协作超时触发后不退出循环(对已中止的 fetch 空转清空队列)→ while 增加 signal aborted 检查
|
|
68
|
+
- 无 body 响应(204/304)导致 `res.body` 迭代抛 TypeError → 空响应防护返回空 buffer
|
|
69
|
+
- offset 续读到文末时误报"登录墙 / SPA 页" → 识别文末,提示"offset 已到文末"
|
|
70
|
+
- playwright 启动失败被永久缓存为 rejected promise(装好后也不重试)→ 失败自愈重置;`closeBrowser` 对未完成的 launch 健壮化
|
|
71
|
+
|
|
72
|
+
**测试**:39 → **42 断言**(+最坏链路预算随配置缩放、yml 字符串强转+钳制、严格宿主 seam 降级)+ test-spa 10 全绿
|
|
73
|
+
|
|
74
|
+
**明确不做**:Web UI 设置卡片——宿主侧注册成功但官方 UI 只渲染自带浏览器半边的插件("A served namespace no card claims renders nothing"),为一个 6 字段低频配置维护 React 构建链不划算;配置走 `cordis.patch.yml` 已足够
|
|
75
|
+
|
|
3
76
|
## [0.4.8] - 2026-08-19
|
|
4
77
|
|
|
5
78
|
### 41 站多类型实测 + 提取修复
|
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/dsh-read-url)
|
|
8
8
|
[](https://github.com/2672243194/dsh-read-url/blob/main/LICENSE)
|
|
9
9
|
|
|
10
|
-
URL reader plugin for DeepSeek Harness: fetch any
|
|
10
|
+
URL reader plugin for DeepSeek Harness: fetch any URL — **webpages (HTML), JSON APIs, RSS/Atom feeds** — auto-detect encoding (UTF-16 BOM / GBK/GB2312 / UTF-8 / Big5 / Shift-JIS), extract the clean main content (**auto-joining paginated articles**), and return **token-efficient compact text or structured Markdown**.
|
|
11
11
|
|
|
12
|
-
Zero dependencies (Node 20+ built-ins), no API key, no server side — install and use.
|
|
12
|
+
Zero runtime dependencies (Node 20+ built-ins handle fetch/decode/extract), no API key, no server side — install and use.
|
|
13
13
|
|
|
14
14
|
## Why
|
|
15
15
|
|
|
@@ -21,7 +21,9 @@ DSH agents can search (getting links and snippets) but lack the step of "reading
|
|
|
21
21
|
|---|---|---|---|---|
|
|
22
22
|
| Body cleaning (container-level) | ❌ whole page | ⚠️ tag-level, nav/footer leak in | ⚠️ custom, noisy | ✅ article/main containers + noise stripping |
|
|
23
23
|
| Default output cap | 200,000 chars | 50,000 chars | 30,000 chars | **6,000 chars + paragraph-aligned truncation** |
|
|
24
|
-
| Chinese GBK/GB2312 | provider-dependent | ⚠️ not normalized, GB2312 garbles | ❌ not handled | ✅ normalized + mojibake fallback |
|
|
24
|
+
| Chinese GBK/GB2312 | provider-dependent | ⚠️ not normalized, GB2312 garbles | ❌ not handled | ✅ normalized + mojibake fallback + UTF-16 BOM + Shift-JIS |
|
|
25
|
+
| JSON / RSS / Atom URLs | ❌ HTML only | ❌ | ❌ | ✅ native compact rendering |
|
|
26
|
+
| Paginated articles | ❌ manual per-page calls | ❌ | ❌ | ✅ auto-joined (default 3 pages) |
|
|
25
27
|
| Session-level cache | ❌ | ❌ | ❌ | ✅ 5-min TTL |
|
|
26
28
|
| `ctx.web` seam | ✅ (official core) | ❌ global fetch | ❌ | ✅ seam-first, fallback included |
|
|
27
29
|
| `ctx.effect` unload cleanup | ✅ | ❌ | ❌ | ✅ |
|
|
@@ -60,6 +62,7 @@ Just talk to the agent:
|
|
|
60
62
|
```
|
|
61
63
|
Read https://example.com/article and summarize the key points
|
|
62
64
|
Read https://docs.example.org/guide in markdown mode
|
|
65
|
+
Read these URLs together and compare their viewpoints: <url1> <url2> <url3>
|
|
63
66
|
```
|
|
64
67
|
|
|
65
68
|
### Real examples (measured)
|
|
@@ -103,7 +106,7 @@ chars 800+800/12398 · cached
|
|
|
103
106
|
|
|
104
107
|
### Tools
|
|
105
108
|
|
|
106
|
-
**`read_url(url, maxChars?, offset?, mode?, includeLinks?)`** — fetch and extract clean body
|
|
109
|
+
**`read_url(url, maxChars?, offset?, mode?, includeLinks?)`** — fetch and extract clean body. Handles HTML pages, JSON APIs (compact re-indented render) and RSS/Atom feeds (entry list + `feedCount`); paginated articles (novel/news/forum) are auto-joined up to `paginateMax` pages; page metadata (`published`, `author`) is harvested automatically — meta tags first, with a byline fallback from the body text when meta is absent
|
|
107
110
|
|
|
108
111
|
| Param | Type | Default | Description |
|
|
109
112
|
|---|---|---|---|
|
|
@@ -149,19 +152,25 @@ chars 800+800/12398 · cached
|
|
|
149
152
|
|
|
150
153
|
### Configuration (optional)
|
|
151
154
|
|
|
152
|
-
|
|
155
|
+
Override via the profile's `cordis.patch.yml` (defaults in the plugin's own `cordis.patch.yml`):
|
|
153
156
|
|
|
154
157
|
```yaml
|
|
155
158
|
- id: dsh-read-url
|
|
156
159
|
config:
|
|
157
|
-
timeoutMs: 15000 # per-request timeout
|
|
160
|
+
timeoutMs: 15000 # per-request timeout (500-120000, clamped)
|
|
158
161
|
maxBytes: 3145728 # response body cap (bytes)
|
|
159
162
|
maxChars: 6000 # default body truncation
|
|
160
163
|
maxLinks: 20 # read_url_links default count
|
|
161
164
|
spaRender: true # SPA rendering enhancement (needs playwright installed; degrades with a hint otherwise)
|
|
165
|
+
paginate: true # auto-join paginated articles
|
|
166
|
+
paginateMax: 3 # pages per article, incl. the first (1-10, clamped)
|
|
162
167
|
userAgent: '...' # request UA
|
|
168
|
+
cacheTtlMs: 300000 # success-cache TTL
|
|
169
|
+
cacheMax: 32 # cache entry cap
|
|
163
170
|
```
|
|
164
171
|
|
|
172
|
+
Values are coerced and clamped to sane ranges at load — quoted numbers in YAML work, garbage falls back to defaults.
|
|
173
|
+
|
|
165
174
|
### Output (compact)
|
|
166
175
|
|
|
167
176
|
```json
|
|
@@ -175,8 +184,14 @@ Plugin-level config is overridable via the profile's `cordis.patch.yml` (default
|
|
|
175
184
|
"truncated": true,
|
|
176
185
|
"charsTotal": 12990,
|
|
177
186
|
"charsReturned": 6000,
|
|
187
|
+
"charsStart": 0,
|
|
178
188
|
"text": "...",
|
|
179
|
-
"
|
|
189
|
+
"rendered": true, // only when extracted after SPA rendering
|
|
190
|
+
"paginated": 3, // pages auto-joined (only when > 1)
|
|
191
|
+
"feedCount": 20, // RSS/Atom entries (feeds only)
|
|
192
|
+
"published": "2026-08-01", // when the page declares it (incl. byline fallback)
|
|
193
|
+
"author": "...", // when the page declares it (incl. byline fallback)
|
|
194
|
+
"links": [] // only when includeLinks=true
|
|
180
195
|
}
|
|
181
196
|
```
|
|
182
197
|
|
|
@@ -200,38 +215,49 @@ const results = await Promise.all([
|
|
|
200
215
|
5. **Two-tier cache** — successful results cached per URL for 5 minutes (repeat reads hit cache: fewer network calls and fewer model retries); **failed results cached for 30 seconds** so a broken URL never triggers a re-fetch loop;
|
|
201
216
|
6. **KV-cache friendly (DeepSeek cost tuning)** — tool schema/description stay **static text** (no config values embedded), so changing config never invalidates the reusable prompt prefix and KV cache keeps hitting. DeepSeek's cache-hit tokens cost about 1/10 of misses — the more stable the prefix, the cheaper the run (same analysis as the official `tool-web` docs);
|
|
202
217
|
7. **Batch shares the cache** — `read_url_batch` reuses the same cache (repeat batches hit it directly) and caps each page at 3,000 chars (below the single-page 6,000) to bound total output;
|
|
203
|
-
8. **Compact fixed cost** — the four tool descriptions total ~
|
|
218
|
+
8. **Compact fixed cost** — the four tool descriptions total ~900 chars (audited by a budget assertion in tests, kept static for KV-cache); extended HTML-entity decoding (45 named entities) means leftovers like `—`/`…` never waste tokens or render as mojibake.
|
|
204
219
|
|
|
205
220
|
## Technical notes
|
|
206
221
|
|
|
207
|
-
- **Encoding**:
|
|
208
|
-
- **
|
|
209
|
-
- **
|
|
210
|
-
- **
|
|
222
|
+
- **Encoding**: BOM-first detection (UTF-8 / UTF-16LE / UTF-16BE — byte-level evidence beats any declared charset), then HTTP `Content-Type` charset → HTML meta; built-in `TextDecoder` transcoding (Node 20+ full-icu, so declared Shift-JIS/EUC-JP/GBK/Big5 all decode), GB2312 normalized to GBK, auto-fallback to UTF-8 on mojibake;
|
|
223
|
+
- **Content dispatch**: a URL is not always an HTML page — JSON APIs are re-rendered compact (indent 1), RSS 2.0 / Atom feeds parse into an entry list (`title — url` + summary, `feedCount` field, full items via `includeLinks`; entry summaries are iteratively tag-stripped and entity-decoded until stable, so double-escaped `<a>` never leaks as a literal tag); XML sitemaps are rejected with a clear reason (no reading value); everything else falls through to the HTML pipeline;
|
|
224
|
+
- **Extraction**: prefers `<article>` (all articles joined on aggregation pages; an unrelated tiny article — e.g. a newsletter card — falls through to `<main>` when its text is under 200 chars and `<main>` exists) / `<main>` / `role="main"`; `role="main"` containers are closed via **depth counting of balanced tags** (nested divs no longer cut the block at the first `</div>` — measured: gnu.org used to lose 7/8 of its body this way); strips `nav/footer/header/aside/form/iframe` and ad-like containers, heuristic fallback to `<body>`; on the body path a **text-density pass** drops link-dominated segments (related posts, category sidebars, hot-article widgets) — pages with standard containers never reach it;
|
|
225
|
+
- **Pagination**: auto-follows `rel=next` or a short next-page anchor (下一页 / next / › / » — conservative, no fuzzy guessing); same-host only, loop-guarded, repeated paragraphs across the seam de-duplicated; continuation pages take the fast static path (a paginated SPA chain would cost one full render per page);
|
|
226
|
+
- **Metadata**: `published` / `author` harvested from meta tags into the output and the status line; when the page has no such meta (ruanyifeng.com ships zero author/date tags) a **byline fallback** harvests the body head (600-char window: `作者:X / 日期:Y` — meta wins, deep-body mentions ignored, markdown link syntax stripped); empty-body pages (login walls, JS shells) fall back to `og:description` as a hint instead of nothing;
|
|
227
|
+
- **Markdown**: self-written lightweight tag state machine (headings/paragraphs/lists/blockquotes/code/tables/inline bold-italic-links), zero deps; images with alt text render as `` (decorative empty-alt images are dropped), code fences carry the language hint from `language-*` highlighter classes;
|
|
228
|
+
- **Safety**: http/https only; no page scripts executed; responses over 3 MB rejected; 15s timeout; **429/503 honored with one Retry-After-aware retry** (capped at 5s so the cooperative timeout still bounds the call); **headerless responses are byte-sniffed** (a NUL byte or >30% control characters marks the body binary — PDFs/images rejected explicitly instead of mojibake through the HTML pipeline); structured errors (HTTP status / timeout / unsupported type / DNS cause such as `getaddrinfo ENOTFOUND` vs blocked-network timeout);
|
|
211
229
|
- **Network fallback (proxy, raced)**: when a proxy is configured (env `HTTPS_PROXY`/`HTTP_PROXY`, falling back to the Windows system proxy registry where Clash-type apps persist it), the plugin starts the direct fetch and a proxy `curl` (`-x` passed explicitly, zero npm deps) **at the same time** and uses whichever completes first — so overseas sites that are blocked on direct connect are served through the user's own proxy in ~0.6s instead of waiting out the direct-connect timeout (~11s measured, -94%). The loser is aborted (curl killed / fetch aborted) and never enters the model context, so **token cost is unchanged**. A failed race returns the original direct error with the proxy attempt noted (`已尝试代理 …`); with no proxy configured the behaviour is exactly the plain direct connect;
|
|
212
230
|
- **Privacy**: the plugin never uses the developer's network configuration — the proxy fallback only reads **your own machine's** proxy (env vars or Windows system proxy) at runtime. No telemetry, no analytics, no data collection: the only outbound action is fetching the URL you asked it to read;
|
|
213
231
|
- **Optional enhancement 1 (Firefox Reader Mode algorithm)**: run `npm i @mozilla/readability happy-dom` in the DSH profile directory to auto-enable `@mozilla/readability` (MPL-2.0, referenced unmodified) for higher-quality extraction; falls back to the built-in heuristic when not installed — the core stays zero-dependency;
|
|
214
|
-
- **Optional enhancement 2 (SPA page rendering)**: run `npm i playwright && npx playwright install chromium` in the DSH profile directory to auto-enable it. When the extracted body is empty and the page is script-heavy (likely Vue/React client-rendered), the plugin automatically renders it with headless Chromium before extracting (a `rendered` flag tells the model); rendering waits for the DOM to stabilize (content stops growing) instead of `networkidle` — heartbeat-polling sites never idle, so this avoids 30s timeouts; when not installed it degrades with a clear install hint, never errors — the core stays zero-dependency;
|
|
232
|
+
- **Optional enhancement 2 (SPA page rendering)**: run `npm i playwright && npx playwright install chromium` in the DSH profile directory to auto-enable it. When the extracted body is empty and the page is script-heavy (likely Vue/React client-rendered) or a JS-redirect shell (few scripts but an empty `<body>`), the plugin automatically renders it with headless Chromium before extracting (a `rendered` flag tells the model); the rendered text is accepted only when it meaningfully beats the static one (≥20 chars from an empty static extraction — short real content is no longer rejected); **bot-challenge defense** — Cloudflare-style interstitials ("Just a moment...") get an extra 8s poll for the auto-redirect, and if the page still looks like a challenge the rendered result is rejected and the static body kept (an interstitial can carry MORE text than the real body — measured 259 vs 135 chars — and must never be mistaken for an improvement); the interstitial DOM never feeds pagination/link extraction either; rendering waits for the DOM to stabilize (content stops growing) instead of `networkidle` — heartbeat-polling sites never idle, so this avoids 30s timeouts; when not installed it degrades with a clear install hint, never errors — the core stays zero-dependency;
|
|
215
233
|
- **Boundaries**: login-walled pages are not readable; SPA pages need the Playwright enhancement; **structured data (e.g. which like-count belongs to which comment) is out of text-extraction scope** — this plugin flattens HTML into readable text, so exact field↔value associations are lost; for precise fields, intercept the page's actual data API (see "Real-world validation" below).
|
|
216
234
|
|
|
217
|
-
## Real-world validation (2026-08-
|
|
235
|
+
## Real-world validation (2026-08-21, v1.0.0)
|
|
236
|
+
|
|
237
|
+
152-site sweep driven by `multi-site.mjs` (committed, re-runnable, 8-way concurrent): **115 OK / 17 expected boundaries (login-walls·captcha·short static pages) / 20 attributed network·anti-bot errors / 0 crashes** (final round with all pre-release fixes in; network-class errors vary ±5 between runs, all environment-attributed). Coverage: CN portals/media, e-commerce (JD/Taobao/Pinduoduo/Suning/Dangdang), video (Bilibili/iQiyi/Youku/Mango), music, games, novels (Qidian/Zongheng/JJWXC legacy GBK), Q&A/forums, government, universities (Tsinghua/PKU/Fudan/SJTU…), Traditional-Chinese TW/HK (PTT/LTN/UDN), JP/KR portals (Yahoo JP/Hatena/goo/naver/daum), overseas tech docs (GitHub/dev.to/react.dev/nodejs.org/rust/go/python), feeds, JSON APIs, encoding stress (GBK/GB2312/Big5/gb18030), anti-bot & network boundaries. All errors are environment-attributed (Wikipedia/Reddit/UDN connect-timeout; W3C/Tieba/NGA/StackOverflow 403; BUPT 412; DNS-fail…) — every one returned as a structured, correctly-attributed error, never a crash.
|
|
218
238
|
|
|
219
|
-
|
|
239
|
+
Sweep-driven pre-release fixes (each locked by unit tests): double-escaped RSS descriptions, headerless binary sniffing, JS-redirect shell rendering, **`role="main"` container cut short by nested divs** (gnu.org 165→800 chars), **tiny `<article>` hijacking the main content** (gitlab 71→800 chars), relaxed render-acceptance threshold.
|
|
240
|
+
|
|
241
|
+
Plus a **15-item DSH acceptance round** (a real agent driving every read_url tool end-to-end): 12 items fully conformant, the 3 findings all closed — ① Cloudflare's "Just a moment..." interstitial carries MORE text than the real body (259 vs 135 chars) and was mistaken for a render improvement → challenge-marker detection + an extra 8s wait for the auto-redirect + interstitial results rejected; ② ruanyifeng.com ships ZERO author/date meta tags, the byline only exists in body text → byline fallback harvesting the body head (verified: `author=阮一峰 published=2026年8月21日`); ③ PDF rejection now attributes clearly: `Unsupported content-type: application/pdf`.
|
|
220
242
|
|
|
221
243
|
| Category | Sites | Result |
|
|
222
244
|
|---|---|---|
|
|
223
245
|
| Portal navigation cleaning | Baidu / QQ / NetEase / Sina / Douban / CSDN / Sohu / Ifeng | ✅ clean text, no CSS noise |
|
|
224
|
-
| SPA rendering | Bilibili / Xiaoheihe / Juejin / QQ News / SSPAI | ✅ `rendered` flag, post-JS body |
|
|
225
|
-
| Multi-article aggregation | Cnblogs / Ruan Yifeng blog | ✅ 800+ chars across articles |
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
| **
|
|
229
|
-
|
|
|
230
|
-
|
|
|
246
|
+
| SPA rendering | Bilibili / Xiaoheihe / Juejin / QQ News / SSPAI / oschina / ThePaper / Xueqiu / Vipshop / TapTap | ✅ `rendered` flag, post-JS body |
|
|
247
|
+
| Multi-article aggregation | Cnblogs / Ruan Yifeng blog / Douban group / Hacker News | ✅ 800+ chars across articles (pagination-joined paginated=2~3) |
|
|
248
|
+
| **Feeds & data APIs (v1.0.0)** | Ruan Yifeng Atom / github.blog Atom / V2EX Atom / Solidot RSS / SSPAI RSS / GitHub API / HN API | ✅ `charset=feed` / `charset=json` compact render |
|
|
249
|
+
| Static doc pages | MDN / Ruan Yifeng / example.com / GitHub / vuejs.org / react.dev / nodejs.org / go.dev / svelte.dev | ✅ clean extraction (example.com = short page, expected) |
|
|
250
|
+
| **Legacy-layout fixes** | gnu.org (nested-div container) / gitlab (tiny-article hijack) | ✅ 800 chars after fix (165 / 71 chars before) |
|
|
251
|
+
| Q&A / forum / encyclopedia | Zhihu column / Hupu / Baidu Zhidao / Baike | ✅ clean text (Zhihu homepage = login-wall, expected) |
|
|
252
|
+
| GBK legacy encoding | ZOL / Dangdang / JJWXC (gb18030) / People.com.cn (GB2312) | ✅ no mojibake |
|
|
253
|
+
| E-commerce / video / music | JD / Taobao / Pinduoduo / Bilibili / iQiyi / NetEase Music / QQ Music | ✅ clean extraction (Amazon CN button page & Kuaishou login-wall, expected) |
|
|
254
|
+
| Government / education | gov.cn / MOE / MIIT / Stats Bureau / 8 universities | ✅ clean; BUPT 412 attributed (WAF) |
|
|
255
|
+
| Network / anti-bot boundary | W3C·Tieba·NGA·StackOverflow (403); Wikipedia·Reddit·UDN (connect-timeout); httpbin (503/binary rejected); DNS-fail (ENOTFOUND) | ✅ accurately attributed errors (HTTP status / timeout / ENOTFOUND) — not plugin defects |
|
|
256
|
+
| offset continuation | Sina News (1,602 chars) | ✅ 800+800 seamless, cache hit |
|
|
231
257
|
| Batch + failure isolation | 4-URL mix | ✅ 2/4 ok, failures isolated |
|
|
232
258
|
| Site crawl | Ruan Yifeng blog | ✅ 5/5 pages tree map |
|
|
233
259
|
|
|
234
|
-
- **
|
|
260
|
+
- **77 zero-dep assertions** (incl. entity decoding, description-budget guard, link dedupe, table-separator escaping, proxy-fallback function, missing-args tolerance, race logic, empty-race guard, schema budget, bare-main pick, worst-case timeout budgets, yml string coercion + clamping, strict-host seam degradation, UTF-16 BOM, Shift-JIS, density filter, pagination join/cap/disable, JSON render, RSS parse, sitemap rejection, 429 Retry-After retry, image alt, code-fence language, metadata, og:description fallback, double-escaped feed, headerless binary sniff, nested role=main, tiny-article fallback, unbalanced-tag degradation, byline fallback, challenge-page detection) + **12 SPA-test assertions** all green;
|
|
235
261
|
- Real case: on a Xiaoheihe post, comment like-counts (`up` field) could not be attributed from flattened text — **precise fields should come from the page's underlying data API** (e.g. `/bbs/app/link/tree` JSON). This is a shared boundary of text extractors, not a defect.
|
|
236
262
|
|
|
237
263
|
## Roadmap
|
|
@@ -240,13 +266,21 @@ const results = await Promise.all([
|
|
|
240
266
|
- [x] On-demand SPA rendering (optional Playwright enhancement, auto-enabled once the browser is installed)
|
|
241
267
|
- [x] Batch reading (`read_url_batch`)
|
|
242
268
|
- [x] Recursive site crawl (`read_url_site`)
|
|
269
|
+
- [x] Native JSON / RSS / Atom support (v1.0.0)
|
|
270
|
+
- [x] Auto-joined paginated articles (v1.0.0)
|
|
271
|
+
- [x] UTF-16 BOM / Shift-JIS encoding boost (v1.0.0)
|
|
272
|
+
- [x] Text-density fallback + metadata harvesting (v1.0.0)
|
|
273
|
+
- [x] Bot-challenge defense (Cloudflare interstitial detection + auto-redirect wait + rejection, v1.0.0)
|
|
274
|
+
- [x] Byline fallback for meta-less pages (author / published, v1.0.0)
|
|
275
|
+
|
|
276
|
+
> From v1.0.0 the plugin enters maintenance mode: bug fixes first, updates kept rare.
|
|
243
277
|
|
|
244
278
|
## Development
|
|
245
279
|
|
|
246
280
|
```bash
|
|
247
281
|
node test.mjs # zero-dependency self-tests (charset/extract/markdown/truncate)
|
|
248
|
-
node test-spa.mjs # SPA rendering tests (
|
|
249
|
-
node multi-site.mjs #
|
|
282
|
+
node test-spa.mjs # SPA rendering tests (12 assertions; SKIPs if playwright absent)
|
|
283
|
+
node multi-site.mjs # 152-site real-world sweep (needs network, CONC=8 tunable): portals/SPA/login-walls/static/feeds/JSON/anti-bot/net-boundaries
|
|
250
284
|
|
|
251
285
|
# End-to-end (requires DSH CLI)
|
|
252
286
|
npx @deepseek-ai/dsh plugin --profile headless add . # run from the parent dir of the plugin
|
package/README.zh.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/dsh-read-url)
|
|
8
8
|
[](https://github.com/2672243194/dsh-read-url/blob/main/LICENSE)
|
|
9
9
|
|
|
10
|
-
DeepSeek Harness 的 URL
|
|
10
|
+
DeepSeek Harness 的 URL 阅读插件:抓取任意 URL——**网页(HTML)、JSON 接口、RSS/Atom 订阅源**——**自动识别编码(UTF-16 BOM / GBK/GB2312 / UTF-8 / Big5 / Shift-JIS)**,提取干净正文(**分页长文自动拼接**),输出**省 token 的紧凑文本或结构化 Markdown**。
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
核心零运行时依赖(Node 20+ 内置能力完成抓取/转码/提取),免 API key,免服务端,装完即用。
|
|
13
13
|
|
|
14
14
|
## 为什么做它
|
|
15
15
|
|
|
@@ -21,13 +21,15 @@ DSH 的 Agent 能搜索(返回链接和片段),但缺"把 URL 读成干净
|
|
|
21
21
|
|---|---|---|---|---|
|
|
22
22
|
| 正文净化(容器级提取) | ❌ 整页渲染 | ⚠️ 标签级去噪,nav/footer 仍混入 | ⚠️ 自研,含噪音 | ✅ article/main 容器 + 噪音剥离 |
|
|
23
23
|
| 默认输出上限 | 200000 字符 | 50000 字符 | 30000 字符 | **6000 字符 + 段落级截断** |
|
|
24
|
-
| 中文 GBK/GB2312 | 视 provider | ⚠️ 未归一化,GB2312 易乱码 | ❌ 未处理 | ✅ 归一化 + 乱码回退 |
|
|
24
|
+
| 中文 GBK/GB2312 | 视 provider | ⚠️ 未归一化,GB2312 易乱码 | ❌ 未处理 | ✅ 归一化 + 乱码回退 + UTF-16 BOM + Shift-JIS |
|
|
25
|
+
| JSON / RSS / Atom URL | ❌ 仅 HTML | ❌ | ❌ | ✅ 原生紧凑渲染 |
|
|
26
|
+
| 分页长文 | ❌ 需逐页手动调用 | ❌ | ❌ | ✅ 自动拼接(默认 3 页) |
|
|
25
27
|
| 会话级缓存 | ❌ | ❌ | ❌ | ✅ 5 分钟 TTL |
|
|
26
28
|
| 走 `ctx.web` seam | ✅ 官方本体 | ❌ 全局 fetch | ❌ | ✅ 优先 seam,缺失回退 |
|
|
27
29
|
| `ctx.effect` 卸载清理 | ✅ | ❌ | ❌ | ✅ |
|
|
28
30
|
| 协作式超时(不暴露给模型) | ✅ | ⚠️ 自管 | ⚠️ 自管 | ✅ `timeoutMs` + `exec.signal` |
|
|
29
31
|
| 模型视角输出 | 整页 Markdown | 紧凑文本 | 15 字段 JSON | **紧凑文本(无需解析 JSON)** |
|
|
30
|
-
| 依赖 | 官方 | TS 需构建 | 零依赖 |
|
|
32
|
+
| 依赖 | 官方 | TS 需构建 | 零依赖 | 核心零运行时依赖(JS ESM 即装即用) |
|
|
31
33
|
| 反爬/降级响应(UA 与 TLS 指纹) | ⚠️ Node 默认 UA,实测 https 被中间设备按 TLS 指纹拦截、百度返回无热搜的降级版 | ❓ 未披露 | ❓ 未披露 | ✅ 完整浏览器 UA,实测获取完整版页面(百度热搜正常) |
|
|
32
34
|
|
|
33
35
|
> 2026-08-16 实测(本机环境):停用本插件后用官方 `web_fetch` 读 `https://www.baidu.com`——TLS 握手被中间设备按程序指纹拦截(退回 http 才成功),且百度对 Node UA 返回**服务端降级版**(热搜词条改由 JS 异步加载,静态 HTML 不含);换回 `dsh-read-url` 后 https 正常、热搜完整可读。差异根因:请求的 UA 与 TLS 特征决定网站/中间设备是否按 bot 处理。
|
|
@@ -104,7 +106,7 @@ chars 800+800/12398 · cached
|
|
|
104
106
|
|
|
105
107
|
### 工具
|
|
106
108
|
|
|
107
|
-
**`read_url(url, maxChars?, offset?, mode?, includeLinks?)`** —
|
|
109
|
+
**`read_url(url, maxChars?, offset?, mode?, includeLinks?)`** — 抓取并提取干净正文。支持 HTML 网页、JSON 接口(紧凑重排渲染)、RSS/Atom 订阅源(条目列表 + `feedCount`);分页长文(小说/新闻/论坛)自动拼接至 `paginateMax` 页;页面元数据(`published`、`author`)自动提取——meta 标签优先,无 meta 时从正文署名行兜底
|
|
108
110
|
|
|
109
111
|
| 参数 | 类型 | 默认 | 说明 |
|
|
110
112
|
|---|---|---|---|
|
|
@@ -150,19 +152,25 @@ chars 800+800/12398 · cached
|
|
|
150
152
|
|
|
151
153
|
### 配置(可选)
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
通过 profile 的 `cordis.patch.yml` 覆盖(默认值见插件自带 `cordis.patch.yml`):
|
|
154
156
|
|
|
155
157
|
```yaml
|
|
156
158
|
- id: dsh-read-url
|
|
157
159
|
config:
|
|
158
|
-
timeoutMs: 15000 #
|
|
160
|
+
timeoutMs: 15000 # 单请求超时(500-120000,自动钳制)
|
|
159
161
|
maxBytes: 3145728 # 响应体上限(字节)
|
|
160
162
|
maxChars: 6000 # 默认正文截断
|
|
161
163
|
maxLinks: 20 # read_url_links 默认条数
|
|
162
164
|
spaRender: true # SPA 渲染增强(需 playwright 已安装,未装自动降级提示)
|
|
165
|
+
paginate: true # 分页长文自动拼接
|
|
166
|
+
paginateMax: 3 # 每篇最多拼接页数,含首页(1-10,自动钳制)
|
|
163
167
|
userAgent: '...' # 请求 UA
|
|
168
|
+
cacheTtlMs: 300000 # 成功缓存 TTL
|
|
169
|
+
cacheMax: 32 # 缓存条目上限
|
|
164
170
|
```
|
|
165
171
|
|
|
172
|
+
配置在加载时统一强转数字并钳制到合理范围——YAML 里加引号的数字也能用,非法值回退默认。
|
|
173
|
+
|
|
166
174
|
### 输出结构(紧凑)
|
|
167
175
|
|
|
168
176
|
```json
|
|
@@ -176,8 +184,14 @@ chars 800+800/12398 · cached
|
|
|
176
184
|
"truncated": true,
|
|
177
185
|
"charsTotal": 12990,
|
|
178
186
|
"charsReturned": 6000,
|
|
187
|
+
"charsStart": 0,
|
|
179
188
|
"text": "……",
|
|
180
|
-
"
|
|
189
|
+
"rendered": true, // 仅 SPA 渲染后提取时出现
|
|
190
|
+
"paginated": 3, // 自动拼接的页数(仅 > 1 时出现)
|
|
191
|
+
"feedCount": 20, // RSS/Atom 条目数(仅订阅源)
|
|
192
|
+
"published": "2026-08-01", // 页面声明了发布时间时(含署名行兜底)
|
|
193
|
+
"author": "……", // 页面声明了作者时(含署名行兜底)
|
|
194
|
+
"links": [] // 仅 includeLinks=true 时
|
|
181
195
|
}
|
|
182
196
|
```
|
|
183
197
|
|
|
@@ -201,38 +215,49 @@ const results = await Promise.all([
|
|
|
201
215
|
5. **双层缓存**——成功结果按 URL 缓存 5 分钟(重复读取直接命中,省网络也省模型重试);**失败结果缓存 30 秒**(坏 URL 不会触发重复 fetch 循环);
|
|
202
216
|
6. **KV Cache 友好(DeepSeek 成本特调)**——工具 schema/description 保持**静态文本**(不嵌入配置值),配置变更不会使可复用的 prompt 前缀失效,KV 缓存持续命中。DeepSeek 缓存命中 token 价格约为未命中的 1/10,前缀越稳定越省钱(官方 `tool-web` 文档同款分析);
|
|
203
217
|
7. **批量共用缓存**——`read_url_batch` 内部复用同一套缓存,重复批量读直接命中,且每页默认 3000 字符(低于单页 6000)控制总量;
|
|
204
|
-
8. **固定开销压缩**——4 个工具 description 合计约
|
|
218
|
+
8. **固定开销压缩**——4 个工具 description 合计约 900 字符(有断言守卫,保持静态利于 KV 缓存);HTML 实体解码扩展至 45 个命名实体,`—`/`…` 等残留不再浪费 token 或显示为乱码。
|
|
205
219
|
|
|
206
220
|
## 技术说明
|
|
207
221
|
|
|
208
|
-
- **编码**:HTTP `Content-Type` charset → HTML meta
|
|
209
|
-
-
|
|
210
|
-
-
|
|
211
|
-
-
|
|
222
|
+
- **编码**:BOM 优先探测(UTF-8 / UTF-16LE / UTF-16BE——字节级证据优先于任何声明),其次 HTTP `Content-Type` charset → HTML meta;内置 `TextDecoder` 转码(Node 20+ full-icu,页面声明的 Shift-JIS/EUC-JP/GBK/Big5 均可正确解码),GB2312 归一为 GBK,检测到乱码自动回退 UTF-8;
|
|
223
|
+
- **内容类型分发**:URL 不一定是 HTML——JSON 接口紧凑重排渲染(缩进 1 格),RSS 2.0 / Atom 订阅源解析为条目列表(`标题 — 链接` + 摘要,`feedCount` 字段,`includeLinks` 时附完整 items;条目摘要迭代「剥标签+解实体」直到稳定,双重转义的 `<a>` 不会漏成字面标签);XML sitemap 明确拒绝(对模型无阅读价值);其余全部走 HTML 管线;
|
|
224
|
+
- **正文提取**:优先 `<article>`(聚合页多篇合并;无关小卡片 article——如订阅挂件——文本不足 200 字符且页面有 `<main>` 时自动回落 main)/ `<main>` / `role="main"`,`role="main"` 容器用**深度计数找平衡闭合标签**(嵌套 div 不会在第一个 `</div>` 被截断——实测 gnu.org 曾因此只取到 1/8 正文);剥离 `nav/footer/header/aside/form/iframe` 及广告类容器,启发式回归到 `<body>`;body 路径上追加**文本密度过滤**——丢弃链接主导的短块(相关推荐/分类侧栏/热门文章挂件),标准容器页面完全不走此路径;
|
|
225
|
+
- **分页拼接**:识别 `rel=next`(标准)或纯「下一页 / next / › / »」短锚文本(刻意保守,不做模糊猜测);同域限定 + 防环;跨页重复段落自动去重;续页走静态快路径(分页 SPA 链每页一次完整渲染不划算);
|
|
226
|
+
- **元数据**:`published` / `author` 从 meta 标签提取进输出字段与状态行;页面无相关 meta 时(如阮一峰博客零 meta 标签)从**正文头部 600 字符的署名行兜底**(「作者:X / 日期:2026年8月21日」,meta 优先、正文深部提及不误采、markdown 链接语法不泄漏);空正文页(登录墙/JS 壳)回落到 `og:description` 作为提示,不再返回空;
|
|
227
|
+
- **Markdown**:自研轻量标签状态机(标题/段落/列表/引用/代码块/表格/行内加粗斜体链接),零依赖;带 alt 的图片渲染为 ``(空 alt 装饰图丢弃),代码块围栏带 `language-*` class 里的语言标注;
|
|
228
|
+
- **安全**:仅 http/https;不执行页面脚本;响应超 3MB 拒绝;15s 超时;**429/503 感知 Retry-After 重试一次**(封顶 5s,保证协作超时仍约束调用);**无 Content-Type 头的响应按字节特征嗅探**(NUL 字节或控制字符占比 >30% 判为二进制,PDF/图片等明确拒绝,不进 HTML 管线产出乱码);错误信息结构化返回(HTTP 状态/超时/类型不支持/DNS 归因如 `getaddrinfo ENOTFOUND` vs 被墙超时);
|
|
212
229
|
- **网络回退(代理,并发竞速)**:检测到代理时(环境变量 `HTTPS_PROXY`/`HTTP_PROXY` → **Windows 系统代理**注册表,Clash 类软件的真正落点),插件**同时发起**直连 fetch 与代理 curl(`-x` 显式传参,零 npm 依赖),**先完成且成功者胜**——海外站(直连被墙)经用户自己的代理 ~0.6s 读到,不再等直连超时兜底(实测 11s → 633ms,-94%)。输者立即 abort(curl 进程 kill / fetch 中止),**结果从不进入模型上下文,token 消耗零变化**。双方均失败时返回原始直连错误并注明代理尝试(`已尝试代理 …`);无代理配置时退化为纯直连(行为与 v0.4.3 完全一致);
|
|
213
230
|
- **隐私**:插件**绝不使用开发者的任何网络配置**——代理回退只在运行时读取**你自己机器**的代理(环境变量或 Windows 系统代理)。无遥测、无统计、无数据收集:唯一的对外动作就是抓取你让它读的那个 URL;
|
|
214
231
|
- **可选增强一(Firefox Reader Mode 算法)**:在 DSH profile 目录执行 `npm i @mozilla/readability happy-dom` 后自动启用,正文提取升级为 `@mozilla/readability`(MPL-2.0,引用不改写),未安装时回退内置启发式提取器,核心保持零依赖;
|
|
215
|
-
- **可选增强二(SPA 页面渲染)**:在 DSH profile 目录执行 `npm i playwright && npx playwright install chromium`
|
|
232
|
+
- **可选增强二(SPA 页面渲染)**:在 DSH profile 目录执行 `npm i playwright && npx playwright install chromium` 后自动启用。检测到正文为空且(页面脚本密集 疑似 Vue/React 客户端渲染,或 body 为空的 JS 跳转壳——script 数不多但正文全靠跳转)时,自动用无头 Chromium 渲染后再提取(`rendered` 标记告知模型);渲染结果仅在**显著优于**静态提取时采用(静态为空时 ≥20 字符即接受,防短正文被拒);**人机验证页防御**——Cloudflare「Just a moment...」等指纹页识别后额外轮询 8s 供其自动跳转,仍未通过则拒绝渲染结果、保留静态正文(指纹页文字量可能超过真实正文,实测 259 vs 135 字符,绝不当作提升),指纹页 DOM 也不污染分页/链接提取;渲染采用 `domcontentloaded` + **DOM 稳定轮询**(内容停止增长即收,上限 10s)而非 `networkidle`——心跳轮询站永不空闲,避免 30s 超时;未安装时优雅提示安装方法、不报错——核心保持零依赖;
|
|
216
233
|
- **边界**:登录墙页面无法读取;SPA 页面需安装 Playwright 增强后渲染读取(未安装时返回明确提示);**结构化数据(如评论的点赞数归属、榜单数值)不在文本提取范围**——本插件把 HTML 扁平化为可读文本,字段与数值的精确对应关系会丢失;需要精确字段时,用 Playwright 拦截页面实际调用的数据 API 获取(见下方「真实世界验证」)。
|
|
217
234
|
|
|
218
|
-
## 真实世界验证(2026-08-
|
|
235
|
+
## 真实世界验证(2026-08-21,v1.0.0)
|
|
236
|
+
|
|
237
|
+
152 站全量实测(`multi-site.mjs` 已提交可复跑,8 并发):**115 OK / 17 预期边界(登录墙·验证页·静态小页) / 20 网络·反爬归因错误 / 0 崩溃**(含全部发布前修复的终态轮;网络类错误逐轮有 ±5 波动,均为环境归因)。覆盖国内门户 / 媒体 / 电商(京东·淘宝·拼多多·苏宁·当当)/ 视频(B 站·爱奇艺·优酷·芒果)/ 音乐 / 游戏 / 小说(起点·纵横·晋江 legacy GBK)/ 问答 / 论坛 / 政府 / 高校(清北复交等 8 所)/ 港台繁体(PTT·自由时报·联合报)/ 日韩(Yahoo JP·Hatena·goo·naver·daum)/ 海外技术站(GitHub·dev.to·react.dev·nodejs·rust·go·python docs)/ 订阅源 / JSON API / 编码压力(GBK·GB2312·Big5·gb18030)/ 反爬与网络边界。错误全部环境归因(维基/Reddit/UDN 连接超时;W3C/贴吧/NGA/StackOverflow 403;北邮 412;DNS 失败等)——每一个都返回结构化、准确归因的错误,无一崩溃。
|
|
219
238
|
|
|
220
|
-
|
|
239
|
+
扫描驱动的发布前修复(全部带单测锁定):RSS 双重转义、无头二进制嗅探、JS 跳转壳渲染、**`role="main"` 嵌套 div 截断**(gnu.org 165→800 字符)、**小 article 劫持主内容**(gitlab 71→800 字符)、渲染接受门槛放宽。
|
|
240
|
+
|
|
241
|
+
另有 **DSH 15 项全功能验收轮**(真实 Agent 调用 read_url 系工具逐项核对):12 项完全符合,3 项问题全部修复闭环——① Cloudflare「Just a moment...」验证页文字量超过真实正文(259 vs 135 字符)被误当作渲染提升 → 挑战页特征识别 + 渲染后额外等待 8s 自动跳转 + 拒绝接受指纹页;② 阮一峰页面零 author/date meta 标签,作者/日期只在正文里 → byline 兜底从正文头部 600 字符提取署名行(实测 `author=阮一峰 published=2026年8月21日`);③ PDF 类型拒绝归因 → 明确报 `Unsupported content-type: application/pdf`。
|
|
221
242
|
|
|
222
243
|
| 类别 | 站点 | 结果 |
|
|
223
244
|
|---|---|---|
|
|
224
245
|
| 门户导航净化 | 百度 / 腾讯 / 网易 / 新浪 / 豆瓣 / CSDN / 搜狐 / 凤凰 | ✅ 干净正文,无 CSS 噪音 |
|
|
225
|
-
| **SPA 渲染** | B 站 / 小黑盒 / 掘金 / QQ 新闻 / 少数派 | ✅ `rendered` 标记 + JS 执行后正文 |
|
|
226
|
-
| 多 article 聚合 | 博客园 / 阮一峰博客 | ✅ 800+
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
|
|
|
246
|
+
| **SPA 渲染** | B 站 / 小黑盒 / 掘金 / QQ 新闻 / 少数派 / 开源中国 / 澎湃 / 雪球 / 唯品会 / TapTap | ✅ `rendered` 标记 + JS 执行后正文 |
|
|
247
|
+
| 多 article 聚合 | 博客园 / 阮一峰博客 / 豆瓣小组 / Hacker News | ✅ 800+ 字符多篇聚合(含分页拼接 paginated=2~3) |
|
|
248
|
+
| **订阅源与数据 API(v1.0.0)** | 阮一峰 Atom / github.blog Atom / v2ex Atom / Solidot RSS / 少数派 RSS / GitHub API / HN API | ✅ `charset=feed` / `charset=json` 紧凑渲染 |
|
|
249
|
+
| 静态文档页 | MDN / 阮一峰 / example.com / GitHub / vuejs.org / react.dev / nodejs.org / go.dev / svelte.dev | ✅ 干净提取(example.com 简单页,预期) |
|
|
250
|
+
| **legacy 布局修复** | gnu.org(嵌套 div 容器)/ gitlab(小 article 劫持) | ✅ 修复后 800 字符满额(修复前 165 / 71 字符) |
|
|
251
|
+
| 问答 / 论坛 / 百科 | 知乎专栏 / 虎扑 / 百度知道 / 百科 | ✅ 干净正文(知乎首页登录墙,预期) |
|
|
252
|
+
| GBK 老编码 | 中关村在线 / 当当 / 晋江(gb18030) / 人民网(GB2312) | ✅ 无乱码 |
|
|
253
|
+
| 电商 / 视频 / 音乐 | 京东 / 淘宝 / 拼多多 / B 站 / 爱奇艺 / 网易云 / QQ 音乐 | ✅ 干净提取(亚马逊跳转按钮页、快手登录墙,预期) |
|
|
254
|
+
| 政府 / 教育 | gov.cn / 教育部 / 工信部 / 统计局 / 清华北大等 8 所高校 | ✅ 干净;北邮 412 已归因(WAF) |
|
|
255
|
+
| 网络·反爬边界 | W3C·贴吧·NGA·StackOverflow(403);维基·Reddit·UDN(连接超时);httpbin(503/二进制拒绝);DNS 失败(ENOTFOUND) | ✅ 错误全部准确归因(HTTP 状态/超时/ENOTFOUND),非插件缺陷 |
|
|
256
|
+
| **offset 续读** | 新浪新闻(1602 字符) | ✅ 800+800 无缝衔接、命中缓存 |
|
|
232
257
|
| **批量 + 失败隔离** | 4 URL 混合 | ✅ 2/4 成功、失败隔离 |
|
|
233
258
|
| **整站爬取** | 阮一峰博客 | ✅ 5/5 页树状站点地图 |
|
|
234
259
|
|
|
235
|
-
- **
|
|
260
|
+
- **77 个单元断言**(含实体解码、description/schema 预算守卫、链接去重、表格分隔行转义、代理回退函数、空参容错、竞速逻辑、空竞速守卫、裸 main 提取、最坏链路超时预算、yml 字符串强转+钳制、严格宿主 seam 降级、UTF-16 BOM、Shift-JIS、密度过滤、分页拼接/封顶/关闭、JSON 渲染、RSS 解析、sitemap 拒绝、429 Retry-After 重试、图片 alt、代码语言、元数据、og:description 回落、双转义 feed、无头二进制嗅探、嵌套 role=main、薄 article 回落、不平衡标签降级、byline 兜底、人机验证页识别)+ **12 个 SPA 测试断言**全绿;
|
|
236
261
|
- 一个真实案例:小黑盒帖子的评论点赞数(`up` 字段)无法从扁平文本确定归属——**精确字段应走页面背后的数据 API**(如 `/bbs/app/link/tree` JSON),这是同类文本提取器的共同边界,不是缺陷。
|
|
237
262
|
|
|
238
263
|
## Roadmap
|
|
@@ -241,17 +266,25 @@ const results = await Promise.all([
|
|
|
241
266
|
- [x] SPA 页面按需渲染(可选 Playwright 增强,装浏览器后自动启用)
|
|
242
267
|
- [x] 批量读取(`read_url_batch`)
|
|
243
268
|
- [x] 整站递归爬取(`read_url_site`)
|
|
269
|
+
- [x] JSON / RSS / Atom 原生支持(v1.0.0)
|
|
270
|
+
- [x] 分页长文自动拼接(v1.0.0)
|
|
271
|
+
- [x] UTF-16 BOM / Shift-JIS 编码增强(v1.0.0)
|
|
272
|
+
- [x] 文本密度兜底 + 元数据提取(v1.0.0)
|
|
273
|
+
- [x] 人机验证页防御(Cloudflare 指纹页识别 + 等待自动跳转 + 拒绝误采,v1.0.0)
|
|
274
|
+
- [x] 无 meta 页面署名行兜底(author / published,v1.0.0)
|
|
275
|
+
|
|
276
|
+
> v1.0.0 起进入维护期:以修 bug 为主,减少更新频率。
|
|
244
277
|
|
|
245
278
|
## 开发
|
|
246
279
|
|
|
247
280
|
```bash
|
|
248
|
-
node test.mjs #
|
|
281
|
+
node test.mjs # 单元自测(转码/提取/Markdown/截断/批量/站点爬取/缓存隔离/配置钳制)
|
|
249
282
|
|
|
250
283
|
# SPA 渲染真实测试(需 playwright 已安装,未装自动 SKIP)
|
|
251
|
-
node test-spa.mjs #
|
|
284
|
+
node test-spa.mjs # 12 断言:JS 正文/渲染后链接/工具不崩溃/缓存隔离/JS 跳转壳
|
|
252
285
|
|
|
253
|
-
#
|
|
254
|
-
node multi-site.mjs # 门户/SPA
|
|
286
|
+
# 152 站真实世界验证(需联网,CONC=8 可调并发)
|
|
287
|
+
node multi-site.mjs # 门户/SPA/登录墙/静态/订阅源/JSON/反爬/网络边界,输出分级结果
|
|
255
288
|
|
|
256
289
|
# 端到端验证(需已安装 DSH CLI)
|
|
257
290
|
npx @deepseek-ai/dsh plugin --profile headless add . # 在插件目录的上一级执行
|