html2rss 0.29.1 → 0.30.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.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/html2rss.gemspec +1 -3
- data/lib/html2rss/batch.rb +3 -3
- data/lib/html2rss/capture/README.md +1 -1
- data/lib/html2rss/capture.rb +1 -1
- data/lib/html2rss/config/request_headers.rb +5 -2
- data/lib/html2rss/config.rb +12 -0
- data/lib/html2rss/doctor/botasaurus.rb +13 -5
- data/lib/html2rss/feed_pipeline/README.md +8 -8
- data/lib/html2rss/feed_pipeline/auto_fallback.rb +1 -1
- data/lib/html2rss/feed_pipeline/strategy_plan.rb +1 -1
- data/lib/html2rss/mcp/README.md +3 -3
- data/lib/html2rss/mcp/contract.rb +9 -5
- data/lib/html2rss/mcp/outcome/playbook.rb +5 -5
- data/lib/html2rss/mcp/server/tools.rb +1 -1
- data/lib/html2rss/mcp/server.rb +1 -1
- data/lib/html2rss/recon.rb +1 -1
- data/lib/html2rss/request_service/botasaurus_strategy.rb +32 -28
- data/lib/html2rss/request_service/compressed_body.rb +13 -8
- data/lib/html2rss/request_service/httpx_strategy.rb +228 -0
- data/lib/html2rss/request_service/response_guard.rb +0 -16
- data/lib/html2rss/request_service/strategy.rb +19 -3
- data/lib/html2rss/request_service.rb +46 -85
- data/lib/html2rss/test.rb +2 -1
- data/lib/html2rss/version.rb +1 -1
- data/lib/html2rss.rb +6 -6
- metadata +6 -40
- data/lib/html2rss/request_service/faraday_strategy.rb +0 -233
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d839914f2d4d26e86998293a431a76168033523857ec8bc65f3f447441cb07e
|
|
4
|
+
data.tar.gz: fd0008a8d7a527b0a2128df3a5603a06c154d61b522f6a14decded9889615b78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 418b9fd5973f19985d7e80abb7734530689bff0753ec948813907f35fd1d2a139ec9fd26353c8c4c87ffc456f446be4a9cdfee9c406022fdfb6c6f0077f886fa
|
|
7
|
+
data.tar.gz: 1a470470f9bdaea585fdccd56e231c3308624e745a98b830bfd1f0861dad4ce22a98c4a2216306cfd893d6d271efc1d68c6efe15ea6e015d43776086db3094ad
|
data/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Most people looking for a first working feed should start with `html2rss-web`, r
|
|
|
13
13
|
Detailed usage guides, reference docs, and the feed directory live on the project website:
|
|
14
14
|
|
|
15
15
|
- [Ruby gem documentation](https://html2rss.github.io/ruby-gem)
|
|
16
|
-
- [Request strategies](https://html2rss.github.io/ruby-gem/reference/strategy) (`auto` = `
|
|
16
|
+
- [Request strategies](https://html2rss.github.io/ruby-gem/reference/strategy) (`auto` = `default` → `botasaurus`; or pin concrete strategies)
|
|
17
17
|
- [Selectors & pagination](https://html2rss.github.io/ruby-gem/reference/selectors#paginated-feeds)
|
|
18
18
|
- [Web application](https://html2rss.github.io/web-application)
|
|
19
19
|
- [Feed directory](https://html2rss.github.io/feed-directory)
|
|
@@ -26,7 +26,7 @@ Cloud development: [Open in GitHub Codespaces](https://github.com/codespaces/new
|
|
|
26
26
|
## Architecture
|
|
27
27
|
|
|
28
28
|
1. **Config** — loads and validates configuration (YAML/hash); schema via `html2rss schema` / `schema/html2rss-config.schema.json`
|
|
29
|
-
2. **RequestService** — fetches pages (`
|
|
29
|
+
2. **RequestService** — fetches pages (`default` (HTTPX), `botasaurus`, or `local_file`)
|
|
30
30
|
3. **Selectors** — extracts content via CSS selectors with extractors/post-processors
|
|
31
31
|
4. **AutoSource** — auto-detects content (Schema.org, JSON state, semantic HTML, structural patterns)
|
|
32
32
|
5. **FeedBuilder** — assembles Article objects and renders feeds (RSS 2.0 / JSON Feed 1.1)
|
|
@@ -157,7 +157,7 @@ Module guide: [`lib/html2rss/mcp/README.md`](lib/html2rss/mcp/README.md).
|
|
|
157
157
|
| ----------------------- | --------------------------------------------------------------- |
|
|
158
158
|
| `html2rss://schema` | Full JSON Schema for feed configurations |
|
|
159
159
|
| `html2rss://extractors` | Registered extractor **names** (options live in schema `$defs`) |
|
|
160
|
-
| `html2rss://strategies` | Published MCP strategies (`auto`, `
|
|
160
|
+
| `html2rss://strategies` | Published MCP strategies (`auto`, `default`, `httpx`, `botasaurus`) |
|
|
161
161
|
| `html2rss://runtime` | `version`, `mcp_contract_version`, `catalog_fingerprint`, `tools`, `botasaurus_configured` (never the scraper URL) |
|
|
162
162
|
|
|
163
163
|
### Prompts
|
|
@@ -183,11 +183,11 @@ Set `BOTASAURUS_SCRAPER_URL` to `http://127.0.0.1:4010` and use strategy `botasa
|
|
|
183
183
|
|
|
184
184
|
| Strategy | Description |
|
|
185
185
|
| ------------ | --------------------------------------------------------------------------- |
|
|
186
|
-
| `auto` | Tries `
|
|
187
|
-
| `
|
|
186
|
+
| `auto` | Tries `default`, falls back to `botasaurus` (default in gem/CLI/MCP scrape) |
|
|
187
|
+
| `default` | Plain HTTP requests via HTTPX (alias: `httpx`; legacy: `faraday`) |
|
|
188
188
|
| `botasaurus` | Puppeteer-backed scraping for JavaScript pages |
|
|
189
189
|
|
|
190
|
-
`inspect` keeps
|
|
190
|
+
`inspect` keeps `default` when `auto` for cheap diagnostics. Elsewhere, strategy can be set via CLI (`--strategy`), gem API keyword argument, or feed config `strategy`. See the [request strategies docs](https://html2rss.github.io/ruby-gem/reference/strategy) for more details.
|
|
191
191
|
|
|
192
192
|
## License
|
|
193
193
|
|
data/html2rss.gemspec
CHANGED
|
@@ -35,9 +35,7 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
spec.add_dependency 'addressable', '~> 2.7'
|
|
36
36
|
spec.add_dependency 'brotli'
|
|
37
37
|
spec.add_dependency 'dry-validation'
|
|
38
|
-
spec.add_dependency '
|
|
39
|
-
spec.add_dependency 'faraday-follow_redirects'
|
|
40
|
-
spec.add_dependency 'faraday-gzip', '~> 3'
|
|
38
|
+
spec.add_dependency 'httpx', '~> 1.8'
|
|
41
39
|
spec.add_dependency 'kramdown'
|
|
42
40
|
spec.add_dependency 'mcp', '~> 1.2'
|
|
43
41
|
spec.add_dependency 'mime-types', '> 3.0'
|
data/lib/html2rss/batch.rb
CHANGED
|
@@ -77,7 +77,7 @@ module Html2rss
|
|
|
77
77
|
# Scrapes multiple URLs in parallel with per-URL error isolation.
|
|
78
78
|
#
|
|
79
79
|
# @param urls [Enumerable<String>] list of URLs to scrape
|
|
80
|
-
# @param strategy [Symbol, String] request strategy (+:auto+, +:
|
|
80
|
+
# @param strategy [Symbol, String] request strategy (+:auto+, +:default+, +:botasaurus+)
|
|
81
81
|
# @param limit [Integer] max articles to extract per URL
|
|
82
82
|
# @param concurrency [Integer] number of worker threads (default 5, max 10)
|
|
83
83
|
# @return [BatchResult]
|
|
@@ -89,7 +89,7 @@ module Html2rss
|
|
|
89
89
|
# Inspects multiple URLs in parallel with per-URL error isolation.
|
|
90
90
|
#
|
|
91
91
|
# @param urls [Enumerable<String>] list of URLs to inspect
|
|
92
|
-
# @param strategy [Symbol, String] request strategy (+:auto+, +:
|
|
92
|
+
# @param strategy [Symbol, String] request strategy (+:auto+, +:default+, +:botasaurus+)
|
|
93
93
|
# @param concurrency [Integer] number of worker threads (default 5, max 10)
|
|
94
94
|
# @return [BatchResult]
|
|
95
95
|
def batch_inspect(urls:, strategy: :auto, concurrency: DEFAULT_CONCURRENCY)
|
|
@@ -104,7 +104,7 @@ module Html2rss
|
|
|
104
104
|
# Runs recon across multiple URLs in parallel with per-URL error isolation.
|
|
105
105
|
#
|
|
106
106
|
# @param urls [Enumerable<String>] list of URLs to recon
|
|
107
|
-
# @param strategy [Symbol, String] request strategy (+:auto+, +:
|
|
107
|
+
# @param strategy [Symbol, String] request strategy (+:auto+, +:default+, +:botasaurus+)
|
|
108
108
|
# @param concurrency [Integer] number of worker threads (default 5, max 10)
|
|
109
109
|
# @option options [String, nil] :cache_dir optional HTML cache directory
|
|
110
110
|
# @return [BatchResult]
|
|
@@ -40,7 +40,7 @@ Html2rss.capture('https://example.com', strategy: :local_file, local_file_path:
|
|
|
40
40
|
Html2rss.capture('https://example.com', max_redirects: 8, max_requests: 4)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`strategy: :auto` uses the same AutoFallback chain as scrape (`
|
|
43
|
+
`strategy: :auto` uses the same AutoFallback chain as scrape (`default` → `botasaurus`). When AutoFallback selects a concrete strategy (or you pin one), Capture **stamps** `strategy:` into the emitted config so later `Html2rss.apply(config)` replays the same transport.
|
|
44
44
|
|
|
45
45
|
## CLI
|
|
46
46
|
|
data/lib/html2rss/capture.rb
CHANGED
|
@@ -75,7 +75,7 @@ module Html2rss
|
|
|
75
75
|
# Analyzes a URL and builds a reusable feed config.
|
|
76
76
|
#
|
|
77
77
|
# @param url [String] source page URL
|
|
78
|
-
# @param strategy [Symbol] request strategy (+:auto+, +:
|
|
78
|
+
# @param strategy [Symbol] request strategy (+:auto+, +:default+, +:botasaurus+)
|
|
79
79
|
# @option options [String, nil] :items_selector optional selector hint
|
|
80
80
|
# @option options [Array<String>, nil] :topics optional directory topics override
|
|
81
81
|
# @option options [String, nil] :title optional title override
|
|
@@ -24,7 +24,6 @@ module Html2rss
|
|
|
24
24
|
DEFAULT_HEADERS = {
|
|
25
25
|
'Accept' => DEFAULT_ACCEPT,
|
|
26
26
|
'Cache-Control' => 'max-age=0',
|
|
27
|
-
'Connection' => 'keep-alive',
|
|
28
27
|
'Sec-Fetch-Dest' => 'document',
|
|
29
28
|
'Sec-Fetch-Mode' => 'navigate',
|
|
30
29
|
'Sec-Fetch-Site' => 'none',
|
|
@@ -33,6 +32,9 @@ module Html2rss
|
|
|
33
32
|
'User-Agent' => DEFAULT_USER_AGENT
|
|
34
33
|
}.freeze
|
|
35
34
|
|
|
35
|
+
# Hop-by-hop headers forbidden in HTTP/2 requests (RFC 7540 §8.1.2.2 / RFC 9113 §8.2.1).
|
|
36
|
+
FORBIDDEN_H2_HEADERS = %w[connection keep-alive proxy-connection transfer-encoding upgrade].to_set.freeze
|
|
37
|
+
|
|
36
38
|
class << self
|
|
37
39
|
##
|
|
38
40
|
# :reek:ManualDispatch
|
|
@@ -75,7 +77,7 @@ module Html2rss
|
|
|
75
77
|
##
|
|
76
78
|
# @return [Hash{String => String}] normalized HTTP headers
|
|
77
79
|
def to_h
|
|
78
|
-
defaults =
|
|
80
|
+
defaults = self.class.browser_defaults
|
|
79
81
|
normalized = normalize_custom_headers(headers)
|
|
80
82
|
|
|
81
83
|
accept_override = normalized.delete('Accept')
|
|
@@ -84,6 +86,7 @@ module Html2rss
|
|
|
84
86
|
defaults['Accept'] = normalize_accept(accept_override)
|
|
85
87
|
defaults['Accept-Language'] = build_accept_language
|
|
86
88
|
|
|
89
|
+
defaults.reject! { |key, _| FORBIDDEN_H2_HEADERS.include?(key.downcase) }
|
|
87
90
|
defaults.compact
|
|
88
91
|
end
|
|
89
92
|
|
data/lib/html2rss/config.rb
CHANGED
|
@@ -93,6 +93,18 @@ module Html2rss
|
|
|
93
93
|
Validator.new.call(prepared_config)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
##
|
|
97
|
+
# Loads and validates a YAML configuration file.
|
|
98
|
+
#
|
|
99
|
+
# @param file [String] the YAML file to load
|
|
100
|
+
# @param feed_name [String, nil] optional feed name for multi-feed files
|
|
101
|
+
# @param multiple_feeds_key [Symbol] key under which multiple feeds are defined
|
|
102
|
+
# @param params [Hash{Symbol => Object, Hash{String => Object, nil}}] dynamic parameters for string formatting
|
|
103
|
+
# @return [Dry::Validation::Result, Html2rss::Config::ValidationResult]
|
|
104
|
+
def validate_yaml(file, feed_name = nil, multiple_feeds_key: MultipleFeedsConfig::CONFIG_KEY_FEEDS, params: UNSET)
|
|
105
|
+
validate(load_yaml(file, feed_name, multiple_feeds_key:), params:)
|
|
106
|
+
end
|
|
107
|
+
|
|
96
108
|
##
|
|
97
109
|
# Returns the packaged JSON Schema file path.
|
|
98
110
|
#
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'httpx'
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
6
|
module Html2rss
|
|
@@ -68,15 +68,23 @@ module Html2rss
|
|
|
68
68
|
# @param base_url [String]
|
|
69
69
|
# @return [Hash{Symbol => Object}]
|
|
70
70
|
def health_check(base_url)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
response
|
|
74
|
-
|
|
71
|
+
endpoint = "#{Url.for_channel(base_url).to_s.chomp('/')}/health"
|
|
72
|
+
response = health_session.get(endpoint)
|
|
73
|
+
raise response.error if response.is_a?(HTTPX::ErrorResponse)
|
|
74
|
+
|
|
75
|
+
body = JSON.parse(response.body.to_s)
|
|
75
76
|
{ ok: response.status == 200, status: response.status, version: body['version'] }
|
|
76
77
|
rescue StandardError => error
|
|
77
78
|
{ ok: false, error: "#{error.class}: #{error.message}" }
|
|
78
79
|
end
|
|
79
80
|
|
|
81
|
+
def health_session
|
|
82
|
+
HTTPX.with(timeout: { operation_timeout: 5, request_timeout: 5, total_request_timeout: 5 },
|
|
83
|
+
resolver_class: :system)
|
|
84
|
+
end
|
|
85
|
+
module_function :health_session
|
|
86
|
+
private_class_method :health_session
|
|
87
|
+
|
|
80
88
|
##
|
|
81
89
|
# @param url [String]
|
|
82
90
|
# @return [Hash{Symbol => Object}]
|
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
`:auto` is the default request plan for feed builds (`auto_source`, `auto_json_feed`, Capture, and MCP `scrape` / `capture`). `FeedPipeline::StrategyPlan` resolves it; `FeedPipeline::AutoFallback` executes `AutoFallback::CHAIN`.
|
|
4
4
|
|
|
5
|
-
Use `:auto` when you want
|
|
5
|
+
Use `:auto` when you want `default` (HTTPX) first and a browser-backed hop only if that fetch fails or yields zero items. Pin a concrete strategy (`default`, `botasaurus`, `local_file`) when you need a single transport.
|
|
6
6
|
|
|
7
7
|
## Chain
|
|
8
8
|
|
|
9
9
|
`AutoFallback::CHAIN` is:
|
|
10
10
|
|
|
11
|
-
1. **
|
|
12
|
-
2. **Botasaurus** — attempted when
|
|
11
|
+
1. **Default (HTTPX)** — plain HTTP (faster, cheaper).
|
|
12
|
+
2. **Botasaurus** — attempted when default raises a fallback-eligible error (for example `BlockedSurfaceDetected` or timeout) or extracts zero feed items.
|
|
13
13
|
|
|
14
|
-
Before escalating
|
|
14
|
+
Before escalating default → Botasaurus on a **weak** auto-source extract (empty / below floor / high-entropy / app-shell / unsupported — not blocked), `FeedResolution` may probe up to five same-origin listing or native-feed candidates and rewrite the effective scrape URL via {ScrapeTarget}. Botasaurus then uses the resolved URL. Direct RSS/Atom entry URLs are parsed via `Syndication::Parser` without HTML AutoSource.
|
|
15
15
|
|
|
16
|
-
There is no Browserless / Puppeteer-in-gem tier. Pin `botasaurus` when you want browser rendering without
|
|
16
|
+
There is no Browserless / Puppeteer-in-gem tier. Pin `botasaurus` when you want browser rendering without default first. Botasaurus needs `BOTASAURUS_SCRAPER_URL`.
|
|
17
17
|
|
|
18
18
|
## Surfaces
|
|
19
19
|
|
|
20
20
|
| Surface | `:auto` behavior |
|
|
21
21
|
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
22
|
-
| Gem / CLI `apply`, MCP `scrape`, Capture | Full AutoFallback chain (`
|
|
23
|
-
| MCP `inspect` | Cheap diagnostic: `StrategyPlan.concrete_for_diagnostic` maps `auto` to
|
|
22
|
+
| Gem / CLI `apply`, MCP `scrape`, Capture | Full AutoFallback chain (`default` → `botasaurus`) |
|
|
23
|
+
| MCP `inspect` | Cheap diagnostic: `StrategyPlan.concrete_for_diagnostic` maps `auto` to default (pin `botasaurus` when you need browser rendering) |
|
|
24
24
|
|
|
25
25
|
## Fallback vs abort
|
|
26
26
|
|
|
@@ -28,7 +28,7 @@ These typically hop to the next chain member (among other `StandardError`s `Auto
|
|
|
28
28
|
|
|
29
29
|
- `Html2rss::RequestService::BlockedSurfaceDetected`
|
|
30
30
|
- `Html2rss::RequestService::RequestTimedOut`
|
|
31
|
-
-
|
|
31
|
+
- HTTPX connection / timeout errors
|
|
32
32
|
- Empty extraction results when a later chain member may succeed
|
|
33
33
|
|
|
34
34
|
These abort immediately (`AutoFallback::NON_FALLBACK_ERRORS`): unknown strategy, invalid URL, unsupported scheme, budget exceeded, private network denied, cross-origin follow-up denied, response too large.
|
|
@@ -8,7 +8,7 @@ module Html2rss
|
|
|
8
8
|
# Hosted by the pipeline instance: session + extract call back into FeedPipeline.
|
|
9
9
|
class AutoFallback # rubocop:disable Metrics/ClassLength -- attempt state + logging stay co-located
|
|
10
10
|
# Ordered list of concrete request strategies attempted by the :auto plan.
|
|
11
|
-
CHAIN = %i[
|
|
11
|
+
CHAIN = %i[default botasaurus].freeze
|
|
12
12
|
|
|
13
13
|
# Deterministic HTTP statuses that indicate permanent failure rather than anti-bot challenges.
|
|
14
14
|
DETERMINISTIC_HTTP_STATUSES = Set[400, 404, 410, 422, 451].freeze
|
|
@@ -42,7 +42,7 @@ module Html2rss
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
##
|
|
45
|
-
# Cheap single-request diagnostic default: +:auto+ → +:
|
|
45
|
+
# Cheap single-request diagnostic default: +:auto+ → +:default+.
|
|
46
46
|
# Does not run {AutoFallback}. Prefer {resolve} for scrape/capture feeds.
|
|
47
47
|
#
|
|
48
48
|
# @param name [Symbol, String, nil] plan name (+nil+ → +:auto+)
|
data/lib/html2rss/mcp/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Batch: `batch_inspect`, `batch_recon`, `batch_scrape`.
|
|
|
20
20
|
|
|
21
21
|
## Decision tree
|
|
22
22
|
|
|
23
|
-
1. **Articles now (no saved config)?** → `scrape` (or `batch_scrape` for multiple URLs). `strategy: "auto"` runs
|
|
23
|
+
1. **Articles now (no saved config)?** → `scrape` (or `batch_scrape` for multiple URLs). `strategy: "auto"` runs default (HTTPX) → Botasaurus; do not retry with explicit `default` after `auto`.
|
|
24
24
|
2. **Reusable feed YAML?** → `capture` → `test` → `apply`. `capture` returns YAML in `payload.yaml`. Strive `enhance: true` (false only when chrome leaks). `test` runs schema + live extraction; optional `compare_enhance` compares enhance off vs on. `apply` is the ship gate (`isError` on zero items). Both `test` and `apply` may include `quality_report.enhance_gains` when `selectors.items.enhance` is true.
|
|
25
25
|
3. **Weak scrape/capture or recon?** → `inspect` (or `batch_inspect`). When alternates warrant it, follow `next_step` to `recon`.
|
|
26
26
|
4. **Config already in hand?** → `validate` (schema only) → `test` → `apply`.
|
|
@@ -61,7 +61,7 @@ Do not duplicate playbook prose in `server.rb`.
|
|
|
61
61
|
| ----------------------- | ---------------------------------------------------------- |
|
|
62
62
|
| `html2rss://schema` | Full JSON Schema for feed configurations |
|
|
63
63
|
| `html2rss://extractors` | Registered extractor names |
|
|
64
|
-
| `html2rss://strategies` | Published MCP strategies (`auto`, `
|
|
64
|
+
| `html2rss://strategies` | Published MCP strategies (`auto`, `default`, `httpx`, `botasaurus`) |
|
|
65
65
|
| `html2rss://runtime` | `version`, `mcp_contract_version`, `catalog_fingerprint`, `tools`, `botasaurus_configured` (never the scraper URL). Fingerprint covers tool names, required keys, and `oneOf` branches; bump `mcp_contract_version` for envelope semantics. |
|
|
66
66
|
|
|
67
67
|
## Prompts
|
|
@@ -73,7 +73,7 @@ Do not duplicate playbook prose in `server.rb`.
|
|
|
73
73
|
|
|
74
74
|
## Strategy note
|
|
75
75
|
|
|
76
|
-
`scrape` / `capture` with `strategy: "auto"` run the full AutoFallback chain. `inspect` maps `auto` to
|
|
76
|
+
`scrape` / `capture` with `strategy: "auto"` run the full AutoFallback chain. `inspect` maps `auto` to default (HTTPX) for cheap diagnostics; pin `botasaurus` when you need browser rendering for inspect.
|
|
77
77
|
|
|
78
78
|
## Inspect redirects
|
|
79
79
|
|
|
@@ -9,7 +9,11 @@ module Html2rss
|
|
|
9
9
|
# annotations, and the single compact JSON envelope response.
|
|
10
10
|
module Contract # rubocop:disable Metrics/ModuleLength -- published listing constants stay co-located
|
|
11
11
|
# Published MCP request strategies (excludes +local_file+).
|
|
12
|
-
STRATEGIES = %w[auto
|
|
12
|
+
STRATEGIES = %w[auto default httpx botasaurus].freeze
|
|
13
|
+
# Accepted migration strategies retained for backwards compatibility.
|
|
14
|
+
MIGRATION_STRATEGIES = %w[faraday].freeze
|
|
15
|
+
# Complete set of strategies accepted by {.assert_published_request!}.
|
|
16
|
+
ALL_ACCEPTED_STRATEGIES = (STRATEGIES + MIGRATION_STRATEGIES).freeze
|
|
13
17
|
|
|
14
18
|
# Bump when tool names, required inputs, or envelope semantics change (independent of gem +VERSION+).
|
|
15
19
|
MCP_CONTRACT_VERSION = 2
|
|
@@ -30,12 +34,12 @@ module Html2rss
|
|
|
30
34
|
type: 'string',
|
|
31
35
|
enum: STRATEGIES,
|
|
32
36
|
default: 'auto',
|
|
33
|
-
description: 'Request strategy (auto runs
|
|
37
|
+
description: 'Request strategy (auto runs default → botasaurus fallback chain)'
|
|
34
38
|
}.freeze
|
|
35
39
|
|
|
36
|
-
# JSON Schema property for inspect +strategy+ (auto stays on
|
|
40
|
+
# JSON Schema property for inspect +strategy+ (auto stays on default).
|
|
37
41
|
INSPECT_STRATEGY_PROPERTY = STRATEGY_PROPERTY.merge(
|
|
38
|
-
description: 'Request strategy (auto uses
|
|
42
|
+
description: 'Request strategy (auto uses default for cheap diagnostics; pin botasaurus when needed)'
|
|
39
43
|
).freeze
|
|
40
44
|
|
|
41
45
|
# JSON Schema +oneOf+ requiring exactly one of +config+ or +yaml+.
|
|
@@ -281,7 +285,7 @@ module Html2rss
|
|
|
281
285
|
# or +request.local_file_path+ is present
|
|
282
286
|
def assert_published_request!(config)
|
|
283
287
|
strategy = config[:strategy]
|
|
284
|
-
unless strategy.nil? ||
|
|
288
|
+
unless strategy.nil? || ALL_ACCEPTED_STRATEGIES.include?(strategy.to_s)
|
|
285
289
|
raise UnpublishedRequestError,
|
|
286
290
|
"MCP does not accept strategy #{strategy} (published: #{STRATEGIES.join(', ')})"
|
|
287
291
|
end
|
|
@@ -36,7 +36,7 @@ module Html2rss
|
|
|
36
36
|
html2rss MCP — decide which tool to call:
|
|
37
37
|
|
|
38
38
|
1. Need articles now (no saved config)? → scrape (or batch_scrape for multiple)
|
|
39
|
-
- strategy "auto" runs
|
|
39
|
+
- strategy "auto" runs default (HTTPX) → Botasaurus AutoFallback. Do not retry with explicit default after auto.
|
|
40
40
|
- Empty scrape is still success (articles-now). Follow next_step / guidance (read_runtime if Botasaurus unset).
|
|
41
41
|
2. Need a reusable feed YAML? → capture → test → apply
|
|
42
42
|
- capture returns YAML inside payload.yaml. Draft only: if destination is html2rss-configs, rewrite for directory.topics and explicit channel title/url. Default enhance follows capture evidence (false when admission_drops show chrome); override only when needed.
|
|
@@ -58,9 +58,9 @@ module Html2rss
|
|
|
58
58
|
# @return [String]
|
|
59
59
|
def scrape_webpage_prompt(url)
|
|
60
60
|
<<~MSG.strip
|
|
61
|
-
Scrape #{url} with scrape (strategy auto). One call is enough — auto already runs
|
|
61
|
+
Scrape #{url} with scrape (strategy auto). One call is enough — auto already runs default then Botasaurus.
|
|
62
62
|
Follow envelope next_step and guidance. Call inspect only if articles are empty/weak or you need diagnostics (final_url, status, scheme_downgrade, alternate_feeds). When inspect finds alternates, follow next_step to recon.
|
|
63
|
-
Do not retry scrape with explicit
|
|
63
|
+
Do not retry scrape with explicit default after auto. Read html2rss://runtime if next_step is read_runtime.
|
|
64
64
|
Return payload.items (not a raw JSON array).
|
|
65
65
|
MSG
|
|
66
66
|
end
|
|
@@ -72,7 +72,7 @@ module Html2rss
|
|
|
72
72
|
<<~MSG.strip
|
|
73
73
|
Build a reusable html2rss feed config for #{url}:
|
|
74
74
|
1) capture — YAML is payload.yaml. Check payload.articles_count, payload.has_selectors, and payload.suggested_channel_url. enhance defaults from admission evidence (false when chrome drops are high). When payload.native_feed is set, follow next_step (done — use the native feed).
|
|
75
|
-
2) Follow next_step. If weak or you need recon, inspect then recon when alternates warrant it. Auto already hops to Botasaurus; do not retry capture with botasaurus unless
|
|
75
|
+
2) Follow next_step. If weak or you need recon, inspect then recon when alternates warrant it. Auto already hops to Botasaurus; do not retry capture with botasaurus unless default was blocked.
|
|
76
76
|
3) test with yaml (or config hash) — schema + live extraction. On :schema failure, validate; on :execution/:min_items, recapture. Read payload.quality_report.enhance_gains when enhance is on; optional compare_enhance compares enhance off vs on without changing shipped RSS.
|
|
77
77
|
4) apply — isError if zero items. Confirm payload.item_count and payload.quality_report (including enhance_gains) before shipping.
|
|
78
78
|
If the destination is html2rss-configs, rewrite the draft for directory.topics and explicit channel title/url. Return YAML.
|
|
@@ -95,7 +95,7 @@ module Html2rss
|
|
|
95
95
|
if data[:blocked_surface] || data[:surface_category].to_s == 'blocked_surface'
|
|
96
96
|
return 'Blocked or anti-bot interstitial likely. Retry scrape with strategy botasaurus once ' \
|
|
97
97
|
'(or CLI inspect --deep when BOTASAURUS_SCRAPER_URL is set). ' \
|
|
98
|
-
'Do not retry explicit
|
|
98
|
+
'Do not retry explicit default after auto.'
|
|
99
99
|
end
|
|
100
100
|
if data[:likely_js_shell]
|
|
101
101
|
return 'JS-rendered shell likely (html_present, zero articles). Use strategy auto or botasaurus; ' \
|
|
@@ -13,7 +13,7 @@ module Html2rss
|
|
|
13
13
|
kind: :url,
|
|
14
14
|
description: 'One-shot article extraction as JSON Feed items. ' \
|
|
15
15
|
'Use when you need articles now without a saved config. ' \
|
|
16
|
-
'strategy "auto" triggers fallback chain (
|
|
16
|
+
'strategy "auto" triggers fallback chain (default → botasaurus) for JS-rendered sites.',
|
|
17
17
|
input_schema: Contract::SCRAPE_INPUT_SCHEMA,
|
|
18
18
|
handler: :scrape_outcome
|
|
19
19
|
},
|
data/lib/html2rss/mcp/server.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Html2rss
|
|
|
10
10
|
# This module maps MCP kwargs to those APIs, then {Outcome} + {Contract} shape the envelope.
|
|
11
11
|
#
|
|
12
12
|
# Strategy note: MCP +auto+ passes through to FeedPipeline AutoFallback
|
|
13
|
-
# (
|
|
13
|
+
# (default → botasaurus). Concrete strategies are used as-is.
|
|
14
14
|
# Botasaurus requires +BOTASAURUS_SCRAPER_URL+.
|
|
15
15
|
module Server # rubocop:disable Metrics/ModuleLength
|
|
16
16
|
# MCP server display name.
|
data/lib/html2rss/recon.rb
CHANGED
|
@@ -111,7 +111,7 @@ module Html2rss
|
|
|
111
111
|
# Runs reconnaissance on a single URL.
|
|
112
112
|
#
|
|
113
113
|
# @param url [String, Html2rss::Url] source page URL
|
|
114
|
-
# @param strategy [Symbol] request strategy (:auto, :
|
|
114
|
+
# @param strategy [Symbol] request strategy (:auto, :default, :botasaurus)
|
|
115
115
|
# @param cache_dir [String, nil] optional directory to cache raw HTML bodies
|
|
116
116
|
# @param cache_mutex [Mutex, nil] optional mutex serializing cache writes
|
|
117
117
|
# @option options [Integer, nil] :max_redirects optional maximum redirects
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'httpx'
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'securerandom'
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ module Html2rss
|
|
|
11
11
|
class BotasaurusStrategy < Strategy
|
|
12
12
|
# Content-Type negotiation for the scrape API transport hop.
|
|
13
13
|
TRANSPORT_ACCEPT = 'application/json'
|
|
14
|
-
# Disable compressed bodies so
|
|
14
|
+
# Disable compressed bodies so HTTPX returns raw JSON without implicit decoding surprises.
|
|
15
15
|
TRANSPORT_ENCODING = 'identity'
|
|
16
16
|
# Correlates each POST /scrape with botasaurus-scrape-api request logs.
|
|
17
17
|
REQUEST_ID_HEADER = 'X-Request-Id'
|
|
@@ -28,10 +28,15 @@ module Html2rss
|
|
|
28
28
|
def post_scrape_request
|
|
29
29
|
request_id = SecureRandom.uuid
|
|
30
30
|
Log.debug("#{self.class}: POST /scrape #{REQUEST_ID_HEADER}=#{request_id}")
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
payload = JSON.generate(contract.request_payload)
|
|
32
|
+
response = client.post(scrape_endpoint, body: payload, headers: post_headers(request_id))
|
|
33
|
+
raise response.error if response.is_a?(HTTPX::ErrorResponse)
|
|
34
|
+
|
|
35
|
+
contract.parse_response(response)
|
|
33
36
|
end
|
|
34
37
|
|
|
38
|
+
def scrape_endpoint = "#{scraper_base_url.to_s.chomp('/')}/scrape"
|
|
39
|
+
|
|
35
40
|
def build_response(parsed_response)
|
|
36
41
|
Response.new(
|
|
37
42
|
body: parsed_response.html,
|
|
@@ -44,22 +49,14 @@ module Html2rss
|
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def raise_from_error!(error)
|
|
47
|
-
|
|
48
|
-
raise_if_timed_out!(error)
|
|
49
|
-
raise BotasaurusServiceError, error.failure_message
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def raise_if_challenge_blocked!(error)
|
|
53
|
-
return unless error.challenge_block?
|
|
52
|
+
raise BlockedSurfaceDetected, "Blocked surface detected: #{error.challenge_message}" if error.challenge_block?
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return unless error.timeout?
|
|
54
|
+
if error.timeout?
|
|
55
|
+
log_timeout!(reason: 'botasaurus_upstream')
|
|
56
|
+
raise RequestTimedOut.new(error.failure_message, timeout_phase: error.timeout_phase)
|
|
57
|
+
end
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
raise RequestTimedOut.new(error.failure_message, timeout_phase: error.timeout_phase)
|
|
59
|
+
raise BotasaurusServiceError, error.failure_message
|
|
63
60
|
end
|
|
64
61
|
|
|
65
62
|
def response_url(final_url)
|
|
@@ -79,8 +76,18 @@ module Html2rss
|
|
|
79
76
|
end
|
|
80
77
|
|
|
81
78
|
def client
|
|
82
|
-
|
|
83
|
-
@client ||=
|
|
79
|
+
timeout = attempt_timeout_seconds.to_f
|
|
80
|
+
@client ||= HTTPX.with(headers: client_headers, timeout: client_timeouts(timeout), resolver_class: :system)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def client_timeouts(timeout)
|
|
84
|
+
{
|
|
85
|
+
operation_timeout: timeout,
|
|
86
|
+
connect_timeout: [ctx.policy.connect_timeout_seconds, timeout].min,
|
|
87
|
+
read_timeout: [ctx.policy.read_timeout_seconds, timeout].min,
|
|
88
|
+
request_timeout: timeout,
|
|
89
|
+
total_request_timeout: timeout
|
|
90
|
+
}
|
|
84
91
|
end
|
|
85
92
|
|
|
86
93
|
def client_headers
|
|
@@ -92,14 +99,7 @@ module Html2rss
|
|
|
92
99
|
end
|
|
93
100
|
|
|
94
101
|
def post_headers(request_id)
|
|
95
|
-
{
|
|
96
|
-
'Content-Type' => 'application/json',
|
|
97
|
-
REQUEST_ID_HEADER => request_id
|
|
98
|
-
}
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def request_options
|
|
102
|
-
{ timeout: attempt_timeout_seconds.to_i }
|
|
102
|
+
{ 'Content-Type' => 'application/json', REQUEST_ID_HEADER => request_id }
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def attempt_timeout_seconds
|
|
@@ -123,6 +123,10 @@ module Html2rss
|
|
|
123
123
|
def translate_connection_error(error)
|
|
124
124
|
raise BotasaurusConnectionFailed, "Botasaurus connection failed: #{error.message}"
|
|
125
125
|
end
|
|
126
|
+
|
|
127
|
+
def timeout_error?(error) = error.is_a?(HTTPX::TimeoutError) || super
|
|
128
|
+
|
|
129
|
+
def connection_error?(error) = error.is_a?(HTTPX::ConnectionError) || error.is_a?(HTTPX::TLSError) || super
|
|
126
130
|
end
|
|
127
131
|
end
|
|
128
132
|
end
|
|
@@ -45,9 +45,12 @@ module Html2rss
|
|
|
45
45
|
|
|
46
46
|
def inflate(raw, encoding)
|
|
47
47
|
case encoding
|
|
48
|
-
when 'gzip'
|
|
49
|
-
|
|
48
|
+
when 'gzip'
|
|
49
|
+
return raw unless raw.start_with?(GZIP_MAGIC)
|
|
50
|
+
|
|
51
|
+
uncompress_gzip(raw)
|
|
50
52
|
when 'br' then try_brotli(raw, require_html: false)
|
|
53
|
+
when 'deflate' then inflate_deflate(raw)
|
|
51
54
|
end
|
|
52
55
|
rescue Zlib::Error, Brotli::Error, ArgumentError
|
|
53
56
|
nil
|
|
@@ -55,12 +58,6 @@ module Html2rss
|
|
|
55
58
|
module_function :inflate
|
|
56
59
|
private_class_method :inflate
|
|
57
60
|
|
|
58
|
-
def uncompress_gzip(raw)
|
|
59
|
-
Zlib::GzipReader.wrap(StringIO.new(raw), encoding: 'ASCII-8BIT', &:read)
|
|
60
|
-
end
|
|
61
|
-
module_function :uncompress_gzip
|
|
62
|
-
private_class_method :uncompress_gzip
|
|
63
|
-
|
|
64
61
|
def inflate_deflate(raw)
|
|
65
62
|
inflater = nil
|
|
66
63
|
Zlib::Inflate.inflate(raw)
|
|
@@ -73,6 +70,14 @@ module Html2rss
|
|
|
73
70
|
module_function :inflate_deflate
|
|
74
71
|
private_class_method :inflate_deflate
|
|
75
72
|
|
|
73
|
+
def uncompress_gzip(raw)
|
|
74
|
+
Zlib::GzipReader.wrap(StringIO.new(raw), encoding: 'ASCII-8BIT', &:read)
|
|
75
|
+
rescue Zlib::Error
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
module_function :uncompress_gzip
|
|
79
|
+
private_class_method :uncompress_gzip
|
|
80
|
+
|
|
76
81
|
def try_brotli(raw, require_html: true)
|
|
77
82
|
inflated = Brotli.inflate(raw)
|
|
78
83
|
return inflated unless require_html
|