dsh-read-url 0.4.9 → 1.1.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 CHANGED
@@ -1,5 +1,74 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.0] - 2026-08-22
4
+
5
+ ### 适配 DSH 0.1.1-rc.2:并行工具调用声明
6
+
7
+ 对 `@deepseek-ai/dsh` 0.1.0-rc.8 → 0.1.1-rc.2 做了逐包静态对比(dsh-tools / dsh-tool-web / dsh-base / dsh-headless / dsh-tool-call-timeout-policy / dsh-agent-loop 的 lib 逐字节一致,唯一变化是 dsh-tool-cordis 新增 host 侧 API——authorization flows、session projections host-only units、image attachment policy、webserver index injections,全部为增量,无删除),插件所依赖的 API 面(`ctx.tools.register` / `ctx.effect` / `ctx.web` seam / `ToolDefinition.timeoutMs` / `exec.signal`)**零破坏性变化**,cordis 核心 4.0.1 两版一致。
8
+
9
+ **新增:`isConcurrencySafe` 并行声明(4 个工具全部启用)**
10
+ - DSH agent loop 会把连续的并行安全工具调用归入有界滚动池——此前 read_url 系工具未声明,即使模型同时发多个请求也被逐个串行执行
11
+ - 全部共享状态审查确认交换性安全:会话缓存 Map(get/set 原子,重叠 miss 最多重复抓一次)、解码器缓存(TextDecoder 无状态)、readability 延迟 import(幂等)、SPA 渲染(共享 browser 单例 + 每次调用独立 `newPage`,page 间隔离)、系统代理检测缓存(读环境变量,双写同值)、爬虫去重集合(调用局部)
12
+ - 效果:多源阅读任务(一次读 3–5 个 URL 对比观点)由串行变并行,墙钟时间按最慢一站计
13
+ - 新增单测:4 工具声明锁定 + 6 路并发 read_url 缓存竞态烟雾测试(每个结果必须携带自己的正文,绝不串扰)
14
+
15
+ **验证**:79 单元断言(+2)+ 12 SPA 断言全绿;新版 DSH `--dump-config` 配置树正常解析;headless 端到端实测 read_url 真实调用成功(example.com 返回正确标题)。
16
+
17
+ ## [1.0.0] - 2026-08-21
18
+
19
+ ### 量大管饱的持久版本:一次把「读网页」这件事做全
20
+
21
+ 目标:省 token 不变的前提下,覆盖绝大多数 URL 类型(HTML / JSON / RSS / Atom / 多页长文 / 各国编码),把后续更新频率降到最低。
22
+
23
+ **新增:内容类型分发(HTML 不再是唯一入口)**
24
+ - JSON 数据 API(如 `api.github.com`)→ 原生紧凑渲染(`JSON.stringify` 缩进 1 格),不再报 "Unsupported content-type"
25
+ - RSS 2.0 / Atom 订阅源 → 解析为紧凑条目列表(`标题 — 链接 + 摘要`),返回 `feedCount`,`includeLinks` 时附完整 items;XML sitemap 仍明确拒绝(对模型无阅读价值)
26
+ - 修复 proxy-fallback 的 content-type 正则:`application/rss+xml` 分隔符是 `+` 不是 `/`,此前被误拦
27
+ - web seam 路径同样分流:seam 返回体以 `{`/`[` 开头时尝试 JSON 渲染
28
+
29
+ **新增:分页长文自动拼接(默认 3 页)**
30
+ - 识别 `rel=next`(标准)或纯「下一页 / 下页 / 后一页 / 下一頁 / next / › / » / older entries」短锚文本——刻意保守,不做模糊猜测
31
+ - 同域限定 + 已见 URL 集合防环;续页走静态快路径(分页 SPA 链每页一次完整渲染不划算);`joinPageText` 去除跨页重复段落/标题(30–300 字符重叠窗口)
32
+ - 输出新增 `paginated` 字段,正文标注 `(已自动拼接 N 页)`;`paginate: false` 关闭,`paginateMax` 可调(1–10)
33
+ - 工具超时预算随之修正:`timeoutMs × (paginateMax + 1) + 45s`(首页 + SPA 渲染 + 续页抓取),随配置缩放
34
+
35
+ **新增:文本密度兜底(无标准容器页)**
36
+ - 无 `<article>`/`<main>` 而退化到整页 body 的页面:按块级标签切块,丢弃「短且链接主导」(正文 < 300 字符且锚文本占比 > 65%)的相关推荐 / 分类侧栏 / 热门文章挂件
37
+ - 标准容器页面完全不走此路径,零行为变化
38
+
39
+ **新增:编码增强**
40
+ - BOM 优先探测:UTF-8 / UTF-16LE / UTF-16BE(字节级证据优先于任何声明——UTF-16 body 用 latin1 读 `<meta>` 探针本来就匹配不上)
41
+ - Shift-JIS 等日韩编码:页面声明 charset 即可由内置 TextDecoder 正确解码(实测日文页面全绿)
42
+
43
+ **新增:网络韧性**
44
+ - 429/503 感知 `Retry-After`(秒形式,封顶 5s 保证协作超时仍约束调用)重试一次
45
+ - 空响应(204/304)沿用 v0.4.9 防护
46
+
47
+ **新增:Markdown / 元数据增强**
48
+ - `<img>` 带 alt+src → `![alt](src)`(哨兵占位法穿过转义层;空 alt 装饰图丢弃——模型反正看不见像素)
49
+ - 代码块围栏带语言标注(识别 `language-*` / `lang-*` class,```js 远比无标注准确)
50
+ - 页级元数据提取:`published`(article:published_time / og:published_time / pubdate / dc.date 等)、`author`(author / og:article:author 等)进输出字段与状态行
51
+ - 空正文页(登录墙 / JS 壳)回落到 `og:description` 作为提示,不再返回空
52
+
53
+ **测试**: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 / 起点·开源中国分页候选站)
54
+
55
+ **发布前实测修复(152 站全量扫描 + 逐站诊断定位)**
56
+ - RSS 描述双重转义(`&lt;a&gt;` 嵌套实体)→ `xmlText` 迭代「剥标签+解实体」直到稳定,正文不再出现字面 `<a href>`
57
+ - 无 Content-Type 头的二进制响应(字节级嗅探:NUL 字节或控制字符占比 > 30%)被当 HTML 解码出乱码 → `looksBinary` 嗅探拒绝,直连与代理路径统一防护
58
+ - JS 跳转壳(script 数低于 SPA 阈值但 body 为空,实测 taptap.cn 形态)不触发渲染 → 空 body 且含任意 `<script>` 即渲染
59
+ - **`role="main"` 容器被非贪婪正则截断**:`<div role="main">` 内嵌套 div 时匹配到第一个 `</div>` 就停(实测 gnu.org 只取到 2700/20000+ 字符,naver.com 只取到 115 字符即 0 文本)→ `tagBlockAt` 深度计数找平衡闭合标签
60
+ - **小 `<article>` 劫持主内容**:页面存在无关小 article(如订阅卡片,实测 about.gitlab.com 71 字符)时优先级高于 `<main>` → article 文本 < 200 字符且页面有 `<main>` 时回落 main
61
+ - **渲染结果接受门槛过严**:静态提取为空时,渲染出 20–50 字符的短正文(JS 壳典型形态)被 `+50` 增量门槛拒绝 → 空静态 + 渲染 ≥ 20 字符即接受
62
+
63
+ **发布前实测修复二(DSH 15 项全功能验收驱动)**
64
+ - **Cloudflare 人机验证页劫持正文**:挑战过渡页("Just a moment...")文字量可超过真实静态正文(实测 259 vs 135 字符),被渲染提升逻辑误当作更优结果 → 三层防御:`looksLikeChallenge` 高特异性特征(cf-chl / cdn-cgi/challenge-platform 等)识别;renderPage 检测到挑战页时额外轮询 8s 等待自动跳转(验证常在数秒内自行通过);仍是指纹页则拒绝渲染结果、保留静态正文 + 明确提示,且挑战页 DOM 不污染分页/链接提取
65
+ - **元数据 byline 兜底**:无任何 author/date meta 的页面(实测阮一峰博客零 meta 标签)从正文头部 600 字符提取「作者:XXX / 日期:2026年8月21日」等署名行——仅填空缺字段、meta 优先、深部提及不误采、markdown 链接语法不泄漏
66
+ - 测试 PDF 样本 URL 失效(africau.edu sample.pdf 已 404)→ 换 mozilla.github.io 稳定样本,PDF 类型拒绝路径恢复验证(明确报 `Unsupported content-type: application/pdf`)
67
+
68
+ **测试(发布前终态)**:**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 类型归因)
69
+
70
+ **发布策略**:大版本收敛——此后以修 bug 为主,减少更新频率。
71
+
3
72
  ## [0.4.9] - 2026-08-20
4
73
 
5
74
  ### 稳定性:配置健壮性 + 严格宿主兼容 + 资源泄漏
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![npm](https://img.shields.io/npm/v/dsh-read-url)](https://www.npmjs.com/package/dsh-read-url)
8
8
  [![License](https://img.shields.io/github/license/2672243194/dsh-read-url)](https://github.com/2672243194/dsh-read-url/blob/main/LICENSE)
9
9
 
10
- URL reader plugin for DeepSeek Harness: fetch any webpage, **auto-detect encoding (GBK/GB2312/UTF-8/Big5)**, extract the clean main content, and return **token-efficient compact text or structured Markdown**.
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
12
  Zero runtime dependencies (Node 20+ built-ins handle fetch/decode/extract), no API key, no server side — install and use.
13
13
 
@@ -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 | ✅ | ❌ | ❌ | ✅ |
@@ -39,7 +41,8 @@ Implemented per official docs (`docs/capability-seams.md`, `docs/cordis-primer.m
39
41
  1. **Web access via the `ctx.web` capability seam** — all web requests go through `ctx.web.fetch()` first (provider resolved inside the seam, same as official `tool-web`), falling back to global fetch when the seam is absent. The network layer is replaceable, not bound to any provider;
40
42
  2. **Reversible side effects** — the session cache is registered under `ctx.effect`, auto-cleared on plugin unload (temporal composability);
41
43
  3. **Cooperative tool-call timeout** — `ToolDefinition.timeoutMs` declares the budget, `execute(args, exec)` forwards `exec.signal` to fetch; the timeout policy is enforced by the pipeline, never exposed to the model;
42
- 4. **Model-facing simplicity** — render emits compact text (`title:` header + body); the model consumes it directly with no JSON parsing. Defaults are the most token-efficient; structured output is opt-in.
44
+ 4. **Model-facing simplicity** — render emits compact text (`title:` header + body); the model consumes it directly with no JSON parsing. Defaults are the most token-efficient; structured output is opt-in;
45
+ 5. **Parallel tool calls** — all four tools declare `isConcurrencySafe` (since v1.1.0): every piece of shared state (session cache, decoder cache, per-render isolated browser pages) is commutative-safe, so the agent loop may fan out several read_url calls into one parallel group — the wall-clock time of a multi-source reading task is that of the slowest site, not the sum.
43
46
 
44
47
  ## Install
45
48
 
@@ -60,6 +63,7 @@ Just talk to the agent:
60
63
  ```
61
64
  Read https://example.com/article and summarize the key points
62
65
  Read https://docs.example.org/guide in markdown mode
66
+ Read these URLs together and compare their viewpoints: <url1> <url2> <url3>
63
67
  ```
64
68
 
65
69
  ### Real examples (measured)
@@ -103,7 +107,7 @@ chars 800+800/12398 · cached
103
107
 
104
108
  ### Tools
105
109
 
106
- **`read_url(url, maxChars?, offset?, mode?, includeLinks?)`** — fetch and extract clean body
110
+ **`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
111
 
108
112
  | Param | Type | Default | Description |
109
113
  |---|---|---|---|
@@ -159,6 +163,8 @@ Override via the profile's `cordis.patch.yml` (defaults in the plugin's own `cor
159
163
  maxChars: 6000 # default body truncation
160
164
  maxLinks: 20 # read_url_links default count
161
165
  spaRender: true # SPA rendering enhancement (needs playwright installed; degrades with a hint otherwise)
166
+ paginate: true # auto-join paginated articles
167
+ paginateMax: 3 # pages per article, incl. the first (1-10, clamped)
162
168
  userAgent: '...' # request UA
163
169
  cacheTtlMs: 300000 # success-cache TTL
164
170
  cacheMax: 32 # cache entry cap
@@ -179,8 +185,14 @@ Values are coerced and clamped to sane ranges at load — quoted numbers in YAML
179
185
  "truncated": true,
180
186
  "charsTotal": 12990,
181
187
  "charsReturned": 6000,
188
+ "charsStart": 0,
182
189
  "text": "...",
183
- "links": [] // only when includeLinks=true
190
+ "rendered": true, // only when extracted after SPA rendering
191
+ "paginated": 3, // pages auto-joined (only when > 1)
192
+ "feedCount": 20, // RSS/Atom entries (feeds only)
193
+ "published": "2026-08-01", // when the page declares it (incl. byline fallback)
194
+ "author": "...", // when the page declares it (incl. byline fallback)
195
+ "links": [] // only when includeLinks=true
184
196
  }
185
197
  ```
186
198
 
@@ -204,38 +216,49 @@ const results = await Promise.all([
204
216
  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;
205
217
  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);
206
218
  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;
207
- 8. **Compact fixed cost** — the four tool descriptions total ~990 chars (audited by a budget assertion in tests, kept static for KV-cache); extended HTML-entity decoding (45 named entities) means leftovers like `&mdash;`/`&hellip;` never waste tokens or render as mojibake.
219
+ 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 `&mdash;`/`&hellip;` never waste tokens or render as mojibake.
208
220
 
209
221
  ## Technical notes
210
222
 
211
- - **Encoding**: three-level detection (HTTP `Content-Type` charset → HTML meta → BOM), built-in `TextDecoder` transcoding (Node 20+ full-icu), GB2312 normalized to GBK, auto-fallback to UTF-8 on mojibake;
212
- - **Extraction**: prefers `<article>` / `role="main"`, strips `nav/footer/header/aside/form/iframe` and ad-like containers, heuristic fallback to `<body>`;
213
- - **Markdown**: self-written lightweight tag state machine (headings/paragraphs/lists/blockquotes/code/tables/inline bold-italic-links), zero deps;
214
- - **Safety**: http/https only; no page scripts executed; responses over 3 MB rejected; 15s timeout; structured errors (HTTP status / timeout / unsupported type / DNS cause such as `getaddrinfo ENOTFOUND` vs blocked-network timeout);
223
+ - **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;
224
+ - **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 `&lt;a&gt;` 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;
225
+ - **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;
226
+ - **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);
227
+ - **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;
228
+ - **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 `![alt](src)` (decorative empty-alt images are dropped), code fences carry the language hint from `language-*` highlighter classes;
229
+ - **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);
215
230
  - **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;
216
231
  - **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;
217
232
  - **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;
218
- - **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;
233
+ - **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;
219
234
  - **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).
220
235
 
221
- ## Real-world validation (2026-08-19, v0.4.8)
236
+ ## Real-world validation (2026-08-21, v1.0.0; re-verified 2026-08-22 against DSH 0.1.1-rc.2)
222
237
 
223
- 41-site sweep driven by `multi-site.mjs` (committed, re-runnable): **23 OK / 8 expected boundaries / 10 network·anti-bot boundaries / 0 crashes** — overseas sites (BBC/V2EX) served in ~1s via the direct+proxy race; doc sites (vuejs.org) fixed via bare-`<main>` picking; wikipedia/httpbin remain clear attributed network boundaries.
238
+ 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.
239
+
240
+ 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.
241
+
242
+ 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`.
224
243
 
225
244
  | Category | Sites | Result |
226
245
  |---|---|---|
227
246
  | Portal navigation cleaning | Baidu / QQ / NetEase / Sina / Douban / CSDN / Sohu / Ifeng | ✅ clean text, no CSS noise |
228
- | SPA rendering | Bilibili / Xiaoheihe / Juejin / QQ News / SSPAI | ✅ `rendered` flag, post-JS body |
229
- | Multi-article aggregation | Cnblogs / Ruan Yifeng blog | ✅ 800+ chars across articles |
230
- | Static doc pages | MDN / Ruan Yifeng / example.com / GitHub | ✅ clean extraction (example.com = short page, expected) |
231
- | Login wall | Zhihu / Weibo | ✅ clear content or empty (expected) |
232
- | **Proxy fallback (overseas)** | BBC Chinese / V2EX | ✅ direct connect blocked → auto-retried through the user's system proxy → clean body |
233
- | Network / anti-bot boundary | W3C (403 for Chrome UA); Wikipedia (403 via proxy); httpbin (503 service-down); PDF (404); DNS-fail (proxy unreachable) | ✅ accurately attributed errors (HTTP status / 403 / 503 / ENOTFOUND) — not plugin defects |
234
- | offset continuation | Sina News (12,284 chars) | ✅ 800+800 seamless, cache hit |
247
+ | SPA rendering | Bilibili / Xiaoheihe / Juejin / QQ News / SSPAI / oschina / ThePaper / Xueqiu / Vipshop / TapTap | ✅ `rendered` flag, post-JS body |
248
+ | Multi-article aggregation | Cnblogs / Ruan Yifeng blog / Douban group / Hacker News | ✅ 800+ chars across articles (pagination-joined paginated=2~3) |
249
+ | **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 |
250
+ | 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) |
251
+ | **Legacy-layout fixes** | gnu.org (nested-div container) / gitlab (tiny-article hijack) | ✅ 800 chars after fix (165 / 71 chars before) |
252
+ | Q&A / forum / encyclopedia | Zhihu column / Hupu / Baidu Zhidao / Baike | ✅ clean text (Zhihu homepage = login-wall, expected) |
253
+ | GBK legacy encoding | ZOL / Dangdang / JJWXC (gb18030) / People.com.cn (GB2312) | ✅ no mojibake |
254
+ | E-commerce / video / music | JD / Taobao / Pinduoduo / Bilibili / iQiyi / NetEase Music / QQ Music | ✅ clean extraction (Amazon CN button page & Kuaishou login-wall, expected) |
255
+ | Government / education | gov.cn / MOE / MIIT / Stats Bureau / 8 universities | ✅ clean; BUPT 412 attributed (WAF) |
256
+ | 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 |
257
+ | offset continuation | Sina News (1,602 chars) | ✅ 800+800 seamless, cache hit |
235
258
  | Batch + failure isolation | 4-URL mix | ✅ 2/4 ok, failures isolated |
236
259
  | Site crawl | Ruan Yifeng blog | ✅ 5/5 pages tree map |
237
260
 
238
- - **42 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) + **10 SPA-test assertions** all green;
261
+ - **79 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, isConcurrencySafe declaration + concurrent cache-race smoke) + **12 SPA-test assertions** all green;
239
262
  - 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.
240
263
 
241
264
  ## Roadmap
@@ -244,13 +267,22 @@ const results = await Promise.all([
244
267
  - [x] On-demand SPA rendering (optional Playwright enhancement, auto-enabled once the browser is installed)
245
268
  - [x] Batch reading (`read_url_batch`)
246
269
  - [x] Recursive site crawl (`read_url_site`)
270
+ - [x] Native JSON / RSS / Atom support (v1.0.0)
271
+ - [x] Auto-joined paginated articles (v1.0.0)
272
+ - [x] UTF-16 BOM / Shift-JIS encoding boost (v1.0.0)
273
+ - [x] Text-density fallback + metadata harvesting (v1.0.0)
274
+ - [x] Bot-challenge defense (Cloudflare interstitial detection + auto-redirect wait + rejection, v1.0.0)
275
+ - [x] Byline fallback for meta-less pages (author / published, v1.0.0)
276
+ - [x] Parallel tool-call declaration (isConcurrencySafe; multi-source wall-clock time = slowest site, v1.1.0)
277
+
278
+ > From v1.0.0 the plugin enters maintenance mode: bug fixes first, updates kept rare.
247
279
 
248
280
  ## Development
249
281
 
250
282
  ```bash
251
283
  node test.mjs # zero-dependency self-tests (charset/extract/markdown/truncate)
252
- node test-spa.mjs # SPA rendering tests (10 assertions; SKIPs if playwright absent)
253
- node multi-site.mjs # 29-site real-world sweep (needs network): portals/SPA/login-walls/static/anti-bot/net-boundaries
284
+ node test-spa.mjs # SPA rendering tests (12 assertions; SKIPs if playwright absent)
285
+ 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
254
286
 
255
287
  # End-to-end (requires DSH CLI)
256
288
  npx @deepseek-ai/dsh plugin --profile headless add . # run from the parent dir of the plugin
package/README.zh.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![npm](https://img.shields.io/npm/v/dsh-read-url)](https://www.npmjs.com/package/dsh-read-url)
8
8
  [![License](https://img.shields.io/github/license/2672243194/dsh-read-url)](https://github.com/2672243194/dsh-read-url/blob/main/LICENSE)
9
9
 
10
- DeepSeek Harness 的 URL 阅读插件:抓取任意网页,**自动识别编码(GBK/GB2312/UTF-8/Big5)**,提取干净正文,输出**省 token 的紧凑文本或结构化 Markdown**。
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
 
@@ -21,7 +21,9 @@ 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` 卸载清理 | ✅ | ❌ | ❌ | ✅ |
@@ -39,7 +41,8 @@ DSH 的 Agent 能搜索(返回链接和片段),但缺"把 URL 读成干净
39
41
  1. **网络访问走 `ctx.web` 能力缝**——所有 web 访问优先通过 `ctx.web.fetch()`(seam 内解析 provider,与官方 `tool-web` 一致),seam 缺失时回退全局 fetch。网络层可替换,不绑定任何具体 provider;
40
42
  2. **可逆副作用**——会话缓存注册在 `ctx.effect` 下,插件卸载即自动清理(时间可组合性);
41
43
  3. **协作式工具调用超时**——`ToolDefinition.timeoutMs` 声明预算,`execute(args, exec)` 把 `exec.signal` 转发给 fetch,超时策略由管线强制执行,不把超时暴露给模型;
42
- 4. **模型视角精简**——render 输出紧凑文本(`title:` 头部 + 正文),模型直接消费,无需解析 JSON;默认参数最省 token,结构化能力按需开启。
44
+ 4. **模型视角精简**——render 输出紧凑文本(`title:` 头部 + 正文),模型直接消费,无需解析 JSON;默认参数最省 token,结构化能力按需开启;
45
+ 5. **并行工具调用**——4 个工具全部声明 `isConcurrencySafe`(v1.1.0 起):共享状态(会话缓存 / 解码器缓存 / SPA 浏览器 page 隔离)均满足交换性安全,agent loop 可把多个 read_url 调用归入并行池——多源阅读任务的墙钟时间按最慢一站计,而非各站之和。
43
46
 
44
47
  ## 安装
45
48
 
@@ -104,7 +107,7 @@ chars 800+800/12398 · cached
104
107
 
105
108
  ### 工具
106
109
 
107
- **`read_url(url, maxChars?, offset?, mode?, includeLinks?)`** — 抓取并提取干净正文
110
+ **`read_url(url, maxChars?, offset?, mode?, includeLinks?)`** — 抓取并提取干净正文。支持 HTML 网页、JSON 接口(紧凑重排渲染)、RSS/Atom 订阅源(条目列表 + `feedCount`);分页长文(小说/新闻/论坛)自动拼接至 `paginateMax` 页;页面元数据(`published`、`author`)自动提取——meta 标签优先,无 meta 时从正文署名行兜底
108
111
 
109
112
  | 参数 | 类型 | 默认 | 说明 |
110
113
  |---|---|---|---|
@@ -160,6 +163,8 @@ chars 800+800/12398 · cached
160
163
  maxChars: 6000 # 默认正文截断
161
164
  maxLinks: 20 # read_url_links 默认条数
162
165
  spaRender: true # SPA 渲染增强(需 playwright 已安装,未装自动降级提示)
166
+ paginate: true # 分页长文自动拼接
167
+ paginateMax: 3 # 每篇最多拼接页数,含首页(1-10,自动钳制)
163
168
  userAgent: '...' # 请求 UA
164
169
  cacheTtlMs: 300000 # 成功缓存 TTL
165
170
  cacheMax: 32 # 缓存条目上限
@@ -180,8 +185,14 @@ chars 800+800/12398 · cached
180
185
  "truncated": true,
181
186
  "charsTotal": 12990,
182
187
  "charsReturned": 6000,
188
+ "charsStart": 0,
183
189
  "text": "……",
184
- "links": [] // 仅 includeLinks=true 时
190
+ "rendered": true, // 仅 SPA 渲染后提取时出现
191
+ "paginated": 3, // 自动拼接的页数(仅 > 1 时出现)
192
+ "feedCount": 20, // RSS/Atom 条目数(仅订阅源)
193
+ "published": "2026-08-01", // 页面声明了发布时间时(含署名行兜底)
194
+ "author": "……", // 页面声明了作者时(含署名行兜底)
195
+ "links": [] // 仅 includeLinks=true 时
185
196
  }
186
197
  ```
187
198
 
@@ -205,38 +216,49 @@ const results = await Promise.all([
205
216
  5. **双层缓存**——成功结果按 URL 缓存 5 分钟(重复读取直接命中,省网络也省模型重试);**失败结果缓存 30 秒**(坏 URL 不会触发重复 fetch 循环);
206
217
  6. **KV Cache 友好(DeepSeek 成本特调)**——工具 schema/description 保持**静态文本**(不嵌入配置值),配置变更不会使可复用的 prompt 前缀失效,KV 缓存持续命中。DeepSeek 缓存命中 token 价格约为未命中的 1/10,前缀越稳定越省钱(官方 `tool-web` 文档同款分析);
207
218
  7. **批量共用缓存**——`read_url_batch` 内部复用同一套缓存,重复批量读直接命中,且每页默认 3000 字符(低于单页 6000)控制总量;
208
- 8. **固定开销压缩**——4 个工具 description 合计约 990 字符(有断言守卫,保持静态利于 KV 缓存);HTML 实体解码扩展至 45 个命名实体,`&mdash;`/`&hellip;` 等残留不再浪费 token 或显示为乱码。
219
+ 8. **固定开销压缩**——4 个工具 description 合计约 900 字符(有断言守卫,保持静态利于 KV 缓存);HTML 实体解码扩展至 45 个命名实体,`&mdash;`/`&hellip;` 等残留不再浪费 token 或显示为乱码。
209
220
 
210
221
  ## 技术说明
211
222
 
212
- - **编码**:HTTP `Content-Type` charset → HTML meta → BOM 三级探测,内置 `TextDecoder` 转码(Node 20+ full-icu),GB2312 归一为 GBK,检测到乱码自动回退 UTF-8;
213
- - **正文提取**:优先 `<article>` / `role="main"`,剥离 `nav/footer/header/aside/form/iframe` 及广告类容器,启发式回归到 `<body>`;
214
- - **Markdown**:自研轻量标签状态机(标题/段落/列表/引用/代码块/表格/行内加粗斜体链接),零依赖;
215
- - **安全**:仅 http/https;不执行页面脚本;响应超 3MB 拒绝;15s 超时;错误信息结构化返回(HTTP 状态/超时/类型不支持/DNS 归因如 `getaddrinfo ENOTFOUND` vs 被墙超时);
223
+ - **编码**: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;
224
+ - **内容类型分发**:URL 不一定是 HTML——JSON 接口紧凑重排渲染(缩进 1 格),RSS 2.0 / Atom 订阅源解析为条目列表(`标题 — 链接` + 摘要,`feedCount` 字段,`includeLinks` 时附完整 items;条目摘要迭代「剥标签+解实体」直到稳定,双重转义的 `&lt;a&gt;` 不会漏成字面标签);XML sitemap 明确拒绝(对模型无阅读价值);其余全部走 HTML 管线;
225
+ - **正文提取**:优先 `<article>`(聚合页多篇合并;无关小卡片 article——如订阅挂件——文本不足 200 字符且页面有 `<main>` 时自动回落 main)/ `<main>` / `role="main"`,`role="main"` 容器用**深度计数找平衡闭合标签**(嵌套 div 不会在第一个 `</div>` 被截断——实测 gnu.org 曾因此只取到 1/8 正文);剥离 `nav/footer/header/aside/form/iframe` 及广告类容器,启发式回归到 `<body>`;body 路径上追加**文本密度过滤**——丢弃链接主导的短块(相关推荐/分类侧栏/热门文章挂件),标准容器页面完全不走此路径;
226
+ - **分页拼接**:识别 `rel=next`(标准)或纯「下一页 / next / › / »」短锚文本(刻意保守,不做模糊猜测);同域限定 + 防环;跨页重复段落自动去重;续页走静态快路径(分页 SPA 链每页一次完整渲染不划算);
227
+ - **元数据**:`published` / `author` 从 meta 标签提取进输出字段与状态行;页面无相关 meta 时(如阮一峰博客零 meta 标签)从**正文头部 600 字符的署名行兜底**(「作者:X / 日期:2026年8月21日」,meta 优先、正文深部提及不误采、markdown 链接语法不泄漏);空正文页(登录墙/JS 壳)回落到 `og:description` 作为提示,不再返回空;
228
+ - **Markdown**:自研轻量标签状态机(标题/段落/列表/引用/代码块/表格/行内加粗斜体链接),零依赖;带 alt 的图片渲染为 `![alt](src)`(空 alt 装饰图丢弃),代码块围栏带 `language-*` class 里的语言标注;
229
+ - **安全**:仅 http/https;不执行页面脚本;响应超 3MB 拒绝;15s 超时;**429/503 感知 Retry-After 重试一次**(封顶 5s,保证协作超时仍约束调用);**无 Content-Type 头的响应按字节特征嗅探**(NUL 字节或控制字符占比 >30% 判为二进制,PDF/图片等明确拒绝,不进 HTML 管线产出乱码);错误信息结构化返回(HTTP 状态/超时/类型不支持/DNS 归因如 `getaddrinfo ENOTFOUND` vs 被墙超时);
216
230
  - **网络回退(代理,并发竞速)**:检测到代理时(环境变量 `HTTPS_PROXY`/`HTTP_PROXY` → **Windows 系统代理**注册表,Clash 类软件的真正落点),插件**同时发起**直连 fetch 与代理 curl(`-x` 显式传参,零 npm 依赖),**先完成且成功者胜**——海外站(直连被墙)经用户自己的代理 ~0.6s 读到,不再等直连超时兜底(实测 11s → 633ms,-94%)。输者立即 abort(curl 进程 kill / fetch 中止),**结果从不进入模型上下文,token 消耗零变化**。双方均失败时返回原始直连错误并注明代理尝试(`已尝试代理 …`);无代理配置时退化为纯直连(行为与 v0.4.3 完全一致);
217
231
  - **隐私**:插件**绝不使用开发者的任何网络配置**——代理回退只在运行时读取**你自己机器**的代理(环境变量或 Windows 系统代理)。无遥测、无统计、无数据收集:唯一的对外动作就是抓取你让它读的那个 URL;
218
232
  - **可选增强一(Firefox Reader Mode 算法)**:在 DSH profile 目录执行 `npm i @mozilla/readability happy-dom` 后自动启用,正文提取升级为 `@mozilla/readability`(MPL-2.0,引用不改写),未安装时回退内置启发式提取器,核心保持零依赖;
219
- - **可选增强二(SPA 页面渲染)**:在 DSH profile 目录执行 `npm i playwright && npx playwright install chromium` 后自动启用。检测到正文为空且页面脚本密集(疑似 Vue/React 客户端渲染)时,自动用无头 Chromium 渲染后再提取(`rendered` 标记告知模型);渲染采用 `domcontentloaded` + **DOM 稳定轮询**(内容停止增长即收,上限 10s)而非 `networkidle`——心跳轮询站永不空闲,避免 30s 超时;未安装时优雅提示安装方法、不报错——核心保持零依赖;
233
+ - **可选增强二(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 超时;未安装时优雅提示安装方法、不报错——核心保持零依赖;
220
234
  - **边界**:登录墙页面无法读取;SPA 页面需安装 Playwright 增强后渲染读取(未安装时返回明确提示);**结构化数据(如评论的点赞数归属、榜单数值)不在文本提取范围**——本插件把 HTML 扁平化为可读文本,字段与数值的精确对应关系会丢失;需要精确字段时,用 Playwright 拦截页面实际调用的数据 API 获取(见下方「真实世界验证」)。
221
235
 
222
- ## 真实世界验证(2026-08-19,v0.4.8)
236
+ ## 真实世界验证(2026-08-21,v1.0.0;2026-08-22 适配 DSH 0.1.1-rc.2 复验)
223
237
 
224
- 41 站全量实测(`multi-site.mjs` 已提交可复跑):**23 OK / 8 预期边界 / 10 网络·反爬边界 / 0 崩溃**——海外站(BBC/V2EX)经竞速通道 ~1s 读回;文档站(vuejs.org)经裸 `<main>` 修复后正文聚焦;wikipedia/httpbin 保持清晰的网络边界归因。
238
+ 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 失败等)——每一个都返回结构化、准确归因的错误,无一崩溃。
239
+
240
+ 扫描驱动的发布前修复(全部带单测锁定):RSS 双重转义、无头二进制嗅探、JS 跳转壳渲染、**`role="main"` 嵌套 div 截断**(gnu.org 165→800 字符)、**小 article 劫持主内容**(gitlab 71→800 字符)、渲染接受门槛放宽。
241
+
242
+ 另有 **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`。
225
243
 
226
244
  | 类别 | 站点 | 结果 |
227
245
  |---|---|---|
228
246
  | 门户导航净化 | 百度 / 腾讯 / 网易 / 新浪 / 豆瓣 / CSDN / 搜狐 / 凤凰 | ✅ 干净正文,无 CSS 噪音 |
229
- | **SPA 渲染** | B 站 / 小黑盒 / 掘金 / QQ 新闻 / 少数派 | ✅ `rendered` 标记 + JS 执行后正文 |
230
- | 多 article 聚合 | 博客园 / 阮一峰博客 | ✅ 800+ 字符多篇聚合 |
231
- | 静态文档页 | MDN / 阮一峰 / example.com / GitHub | ✅ 干净提取(example.com 简单页,预期) |
232
- | 登录墙 | 知乎 / 微博 | ✅ 清晰内容或空(预期) |
233
- | **代理回退(海外站)** | BBC 中文 / V2EX | ✅ 直连被墙 → 自动经系统代理重试 → 干净正文 |
234
- | 网络·反爬边界 | W3C(对 Chrome UA 403);维基(代理 403);httpbin(503 服务端故障);PDF(404);DNS 失败(代理未连通) | ✅ 错误全部准确归因(HTTP 状态/403/503/ENOTFOUND),非插件缺陷 |
235
- | **offset 续读** | 新浪新闻(12284 字符) | ✅ 800+800 无缝衔接、命中缓存 |
247
+ | **SPA 渲染** | B 站 / 小黑盒 / 掘金 / QQ 新闻 / 少数派 / 开源中国 / 澎湃 / 雪球 / 唯品会 / TapTap | ✅ `rendered` 标记 + JS 执行后正文 |
248
+ | 多 article 聚合 | 博客园 / 阮一峰博客 / 豆瓣小组 / Hacker News | ✅ 800+ 字符多篇聚合(含分页拼接 paginated=2~3) |
249
+ | **订阅源与数据 API(v1.0.0)** | 阮一峰 Atom / github.blog Atom / v2ex Atom / Solidot RSS / 少数派 RSS / GitHub API / HN API | ✅ `charset=feed` / `charset=json` 紧凑渲染 |
250
+ | 静态文档页 | MDN / 阮一峰 / example.com / GitHub / vuejs.org / react.dev / nodejs.org / go.dev / svelte.dev | ✅ 干净提取(example.com 简单页,预期) |
251
+ | **legacy 布局修复** | gnu.org(嵌套 div 容器)/ gitlab(小 article 劫持) | ✅ 修复后 800 字符满额(修复前 165 / 71 字符) |
252
+ | 问答 / 论坛 / 百科 | 知乎专栏 / 虎扑 / 百度知道 / 百科 | ✅ 干净正文(知乎首页登录墙,预期) |
253
+ | GBK 老编码 | 中关村在线 / 当当 / 晋江(gb18030) / 人民网(GB2312) | ✅ 无乱码 |
254
+ | 电商 / 视频 / 音乐 | 京东 / 淘宝 / 拼多多 / B 站 / 爱奇艺 / 网易云 / QQ 音乐 | ✅ 干净提取(亚马逊跳转按钮页、快手登录墙,预期) |
255
+ | 政府 / 教育 | gov.cn / 教育部 / 工信部 / 统计局 / 清华北大等 8 所高校 | ✅ 干净;北邮 412 已归因(WAF) |
256
+ | 网络·反爬边界 | W3C·贴吧·NGA·StackOverflow(403);维基·Reddit·UDN(连接超时);httpbin(503/二进制拒绝);DNS 失败(ENOTFOUND) | ✅ 错误全部准确归因(HTTP 状态/超时/ENOTFOUND),非插件缺陷 |
257
+ | **offset 续读** | 新浪新闻(1602 字符) | ✅ 800+800 无缝衔接、命中缓存 |
236
258
  | **批量 + 失败隔离** | 4 URL 混合 | ✅ 2/4 成功、失败隔离 |
237
259
  | **整站爬取** | 阮一峰博客 | ✅ 5/5 页树状站点地图 |
238
260
 
239
- - **42 个单元断言**(含实体解码、description/schema 预算守卫、链接去重、表格分隔行转义、代理回退函数、空参容错、竞速逻辑、空竞速守卫、裸 main 提取、最坏链路超时预算、yml 字符串强转+钳制、严格宿主 seam 降级)+ **10 个 SPA 测试断言**全绿;
261
+ - **79 个单元断言**(含实体解码、description/schema 预算守卫、链接去重、表格分隔行转义、代理回退函数、空参容错、竞速逻辑、空竞速守卫、裸 main 提取、最坏链路超时预算、yml 字符串强转+钳制、严格宿主 seam 降级、UTF-16 BOM、Shift-JIS、密度过滤、分页拼接/封顶/关闭、JSON 渲染、RSS 解析、sitemap 拒绝、429 Retry-After 重试、图片 alt、代码语言、元数据、og:description 回落、双转义 feed、无头二进制嗅探、嵌套 role=main、薄 article 回落、不平衡标签降级、byline 兜底、人机验证页识别、isConcurrencySafe 声明 + 并发缓存竞态烟雾)+ **12 个 SPA 测试断言**全绿;
240
262
  - 一个真实案例:小黑盒帖子的评论点赞数(`up` 字段)无法从扁平文本确定归属——**精确字段应走页面背后的数据 API**(如 `/bbs/app/link/tree` JSON),这是同类文本提取器的共同边界,不是缺陷。
241
263
 
242
264
  ## Roadmap
@@ -245,6 +267,15 @@ const results = await Promise.all([
245
267
  - [x] SPA 页面按需渲染(可选 Playwright 增强,装浏览器后自动启用)
246
268
  - [x] 批量读取(`read_url_batch`)
247
269
  - [x] 整站递归爬取(`read_url_site`)
270
+ - [x] JSON / RSS / Atom 原生支持(v1.0.0)
271
+ - [x] 分页长文自动拼接(v1.0.0)
272
+ - [x] UTF-16 BOM / Shift-JIS 编码增强(v1.0.0)
273
+ - [x] 文本密度兜底 + 元数据提取(v1.0.0)
274
+ - [x] 人机验证页防御(Cloudflare 指纹页识别 + 等待自动跳转 + 拒绝误采,v1.0.0)
275
+ - [x] 无 meta 页面署名行兜底(author / published,v1.0.0)
276
+ - [x] 并行工具调用声明(isConcurrencySafe,多源阅读墙钟时间按最慢一站计,v1.1.0)
277
+
278
+ > v1.0.0 起进入维护期:以修 bug 为主,减少更新频率。
248
279
 
249
280
  ## 开发
250
281
 
@@ -252,10 +283,10 @@ const results = await Promise.all([
252
283
  node test.mjs # 单元自测(转码/提取/Markdown/截断/批量/站点爬取/缓存隔离/配置钳制)
253
284
 
254
285
  # SPA 渲染真实测试(需 playwright 已安装,未装自动 SKIP)
255
- node test-spa.mjs # 10 断言:JS 正文/渲染后链接/工具不崩溃/缓存隔离
286
+ node test-spa.mjs # 12 断言:JS 正文/渲染后链接/工具不崩溃/缓存隔离/JS 跳转壳
256
287
 
257
- # 29 站真实世界验证(需联网)
258
- node multi-site.mjs # 门户/SPA/登录墙/静态/反爬/网络边界,输出分级结果
288
+ # 152 站真实世界验证(需联网,CONC=8 可调并发)
289
+ node multi-site.mjs # 门户/SPA/登录墙/静态/订阅源/JSON/反爬/网络边界,输出分级结果
259
290
 
260
291
  # 端到端验证(需已安装 DSH CLI)
261
292
  npx @deepseek-ai/dsh plugin --profile headless add . # 在插件目录的上一级执行
package/cordis.patch.yml CHANGED
@@ -9,4 +9,6 @@
9
9
  cacheTtlMs: 300000
10
10
  cacheMax: 32
11
11
  spaRender: true
12
+ paginate: true
13
+ paginateMax: 3
12
14
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'