@neuxmind/web-fetch
v0.0.1
Published
High-performance webpage fetch CLI for agents
Readme
Web Fetch CLI
一个面向 Agent / LLM 使用场景的网页抓取工具集,支持把网页转换成适合模型消费的 Markdown / JSON。
当前版本:v0.0.1
当前能力
- 默认优先
https,必要时按规则回退http - 支持单 URL、文件输入、stdin 输入
- 支持
md/json/both - 支持
http/browser/auto - 支持并发抓取
- 支持本地文件缓存:TTL +
ETag/Last-Modifiedrevalidate - 支持批量输出到 stdout / file / jsonl / files
- 支持最小可用 server:
GET /health、POST /fetch、POST /fetch/batch - 默认约束“页面内容拿到后”的处理预算为
3000ms - 支持配置文件驱动的正文抽取
- 支持 LLM 正文抽取,按 provider 顺序降级
- 每个 LLM provider 最多重试 3 次,重试次数可配置
- LLM 全部失败时可回退启发式抽取
- 内置真实网页 live suite:国外 100、中国 100、browser 20
项目结构
crates/
browser-client/
cli/
core/
extractor/
schemas/
server/
transform/
browser-worker/
docs/
packages/npm/
web-fetch.example.toml构建
要求:
- Rust / Cargo
- Node.js(仅 browser worker 需要)
cargo build全量回归测试
统一回归脚本:
chmod +x scripts/run-full-regression.sh
scripts/run-full-regression.sh --config ~/.config/web-fetch/web-fetch.toml特点:
- 走 LLM 全流程
- 同时覆盖正常抓取与 browser 抓取
- 默认并发
5 - LLM provider 由配置文件控制
- 输出落盘到
tests/results/full-regression/<timestamp> - 自动生成
summary.md/summary.json/timing-breakdown.csv summary包含 wall clock、吞吐量和分段时延指标
npm 安装
推荐公开安装方式:
npm install -g @neuxmind/web-fetch安装后可直接使用:
web-fetch --version
web-fetch-server --helpnpm 安装会:
- 自动按平台安装对应预编译二进制包
- 同时提供
web-fetch与web-fetch-server - 初始化默认配置到
~/.config/web-fetch/web-fetch.toml - 若默认配置已存在,则保留原文件不覆盖
脚本安装
macOS / Linux
curl -fsSL https://git.neuxnet.com/cli/web-fetch-cli/-/raw/main/scripts/install.sh | sh安装脚本会自动:
- 识别平台与架构
- 优先下载对应的预编译包
- 若当前版本还没有预编译包,则自动回退到源码编译安装
- 默认安装到
~/.local/bin - 同时安装
web-fetch与web-fetch-server - 初始化默认配置到
~/.config/web-fetch/web-fetch.toml - 若默认配置已存在,则保留原文件不覆盖
可选环境变量:
WEB_FETCH_VERSION=v0.0.1WEB_FETCH_REF=mainWEB_FETCH_INSTALL_DIR=$HOME/.local/binWEB_FETCH_REPO_BASE=https://git.neuxnet.com/cli/web-fetch-cli
例如:
curl -fsSL https://git.neuxnet.com/cli/web-fetch-cli/-/raw/main/scripts/install.sh | \
WEB_FETCH_INSTALL_DIR=/usr/local/bin shWindows PowerShell
irm https://git.neuxnet.com/cli/web-fetch-cli/-/raw/main/scripts/install.ps1 | iex安装后可验证:
web-fetch --version
web-fetch-server --helpnpm 打包
发布 npm 产物:
node scripts/package-npm.mjs只为指定目标生成:
node scripts/package-npm.mjs --targets aarch64-apple-darwin产物输出到:
dist/npm/npm 自动发布
仓库支持通过 Git tag 自动触发 npm 发布:
- 触发条件:tag 名称以
v开头 - 例如:
v0.0.1 - 发布目标:
@neuxmind/web-fetch
发布流程:
- 各平台 runner 分别构建并打包平台 npm 包
- 汇总 tarball
- 先发布平台包
- 最后发布主包
@neuxmind/web-fetch
当前 GitLab CI 配置文件:
.gitlab-ci.yml需要准备:
- GitLab CI/CD 变量:
NPM_TOKEN - 可选 GitLab CI/CD 变量:
NPM_DIST_TAG - 可用 runner tags:
macos-arm64macos-x64linux-arm64linux-x64windows-arm64windows-x64
发布前会校验:
CI_COMMIT_TAG去掉前缀v后- 必须等于根目录
package.json中的version
创建并推送 tag:
git tag v0.0.1
git push origin v0.0.1CLI 用法
最简单示例
输出 JSON:
web-fetch https://example.com --pretty输出 Markdown:
web-fetch https://example.com --format md批量抓取:
web-fetch --input urls.txt --concurrency 16 --output out/results.jsonl --jsonl从 stdin 读取:
cat urls.txt | web-fetch --stdin --format json常用参数
--config <path>--format md|json|both--render auto|http|browser--scheme auto|https|http--selector <css>--timeout <ms>--header key:value--user-agent <ua>--concurrency <n>--cache-dir <path>--cache-ttl <seconds>--cache-mode default|refresh|only-if-cached--no-cache--output <path>--output-mode auto|stdout|jsonl|files--failures-output <path>
完整帮助:
web-fetch --help配置文件
CLI 和 server 都支持配置文件:
- 显式指定:
--config /path/to/web-fetch.toml - 未显式指定时,默认优先加载:
~/.config/web-fetch/web-fetch.toml
可参考:web-fetch.example.toml
建议初始化默认配置:
mkdir -p ~/.config/web-fetch
curl -fsSL https://git.neuxnet.com/cli/web-fetch-cli/-/raw/main/web-fetch.example.toml \
-o ~/.config/web-fetch/web-fetch.toml配置优先级
- 配置文件提供默认值
- CLI 参数覆盖配置文件中的请求默认值
- 请求体中的显式字段覆盖 server 端默认值
示例
[server]
host = "127.0.0.1"
port = 8080
request_timeout_ms = 18000
batch_request_timeout_ms = 30000
max_batch_size = 32
max_batch_concurrency = 8
[browser]
worker_url = "http://127.0.0.1:4000"
[request_defaults]
render_mode = "auto"
scheme_policy = "auto"
timeout_ms = 15000
max_body_size = 5242880
cache_mode = "default"
cache_ttl_secs = 1800
cache_dir = ".web-fetch-cache"
user_agent = "web-fetch-cli/0.0.1"
[extraction]
mode = "llm"
fallback_to_heuristic = true
processing_timeout_ms = 3000
max_input_chars = 1500
[extraction.llm]
prompt = "You are an extraction engine for agents. Return strict JSON only, no markdown fence, no commentary. You will receive candidate_markdown generated by a local heuristic extractor, not raw HTML. The JSON schema is {\"title\": string|null, \"byline\": string|null, \"published_at\": string|null, \"lang\": string|null, \"site_name\": string|null, \"markdown\": string}. Clean and keep only meaningful page content: the article body, documentation body, product details, or the central list/search results that carry user-visible information. Remove navigation menus, site chrome, headers, footers, sidebars, breadcrumbs, ads, cookie banners, login/account widgets, newsletter/signup prompts, social/share buttons, related/recommended links, comment areas, duplicated text, scripts, styles, tracking/legal boilerplate, and decorative images. Preserve useful headings, paragraphs, lists, code blocks, tables, and essential links in markdown. If the page is a portal/index page, keep only the meaningful content cards/items and summaries, not global menus or repeated module titles."
circuit_breaker_threshold = 3
circuit_breaker_open_ms = 30000
slow_request_threshold_ms = 800
slow_request_degrade_threshold = 2
[[extraction.llm.providers]]
name = "primary"
base_url = "https://api.openai.com/v1"
model = "gpt-4.1-mini"
api_key_env = "OPENAI_API_KEY"
retries = 1
timeout_ms = 1200
[extraction.llm.providers.extra_body]
response_format = { type = "json_object" }
[[extraction.llm.providers]]
name = "backup"
base_url = "https://openrouter.ai/api/v1"
model = "openai/gpt-4.1-mini"
api_key_env = "OPENROUTER_API_KEY"
retries = 1
timeout_ms = 1000
[extraction.llm.providers.extra_body]
response_format = { type = "json_object" }LLM 正文抽取
工作方式
当配置:
[extraction]
mode = "llm"抽取器会:
- 先用本地 heuristic 从 HTML 中抽取候选正文块
- 把候选正文块转换成受
max_input_chars约束的candidate_markdown - 准备
candidate_markdown和 metadata hints - 按
providers顺序调用 OpenAI-compatible/chat/completions - 单个 provider 按配置重试
- 单个 provider 的最大实际重试次数会被限制在
3 - 当前 provider 失败后自动降级到下一个 provider
- 全部 provider 失败时,如果
fallback_to_heuristic = true,则回退启发式抽取
为了控制速度和 token 成本,LLM 不再直接处理原始 HTML;它只处理本地抽取出来的候选 Markdown。
3 秒处理预算
为了服务端延迟可控,默认按 processing_timeout_ms = 3000 执行。
含义是:
- 网页 HTML / browser DOM 一旦拿到
- 后续正文抽取、LLM 调用、markdown 转换等处理
- 默认必须在
3000ms内结束
如果超时:
fallback_to_heuristic = true:快速降级到 heuristicfallback_to_heuristic = false:直接返回 timeout 错误
建议在严格低延迟场景下:
- provider
retries设为1 - 单 provider
timeout_ms设为800~1200 max_input_chars设为800~1500- 保留 heuristic fallback
某些会输出可见推理的模型,建议通过 provider extra_body 关闭 thinking / reasoning。例如:
[[extraction.llm.providers]]
name = "tokenhubpro-qwen"
base_url = "https://tokenhubpro.com/v1"
model = "qwen/qwen3.5-9b"
api_key_env = "TOKENHUBPRO_API_KEY"
retries = 0
timeout_ms = 12000
[extraction.llm.providers.extra_body]
chat_template_kwargs = { enable_thinking = false }provider 配置项
namebase_urlmodelapi_key或api_key_envretriestimeout_msheadersextra_body
provider 健康度与自动降级
当前实现会在进程内维护 provider 运行时健康度:
- 连续失败达到
circuit_breaker_threshold后,provider 会熔断circuit_breaker_open_ms - 连续慢请求达到
slow_request_degrade_threshold后,provider 会被标记为degraded - 后续请求会优先尝试健康 provider,再尝试 degraded provider
相关配置位于 [extraction.llm]:
circuit_breaker_thresholdcircuit_breaker_open_msslow_request_threshold_msslow_request_degrade_threshold
返回格式要求
上游 LLM 需要返回严格 JSON,字段为:
titlebylinepublished_atlangsite_namemarkdown
为增强 OpenAI-compatible provider 兼容性,当前实现也会接受常见正文别名:
contenttextbody
输出逻辑
CLI 支持 4 种输出模式:
stdoutfilejsonlfiles
默认 --output-mode auto。
stdout
web-fetch https://example.com --pretty单文件
web-fetch https://example.com --output out/result.json --prettyJSONL
web-fetch \
--input urls.txt \
--output out/results.jsonl \
--jsonl目录输出
web-fetch \
--input urls.txt \
--format both \
--output-mode files \
--output out/pages输出示例:
out/pages/
0001-example-com.json
0001-example-com.md
0002-docs-python-org-3-tutorial-index-html.json
0002-docs-python-org-3-tutorial-index-html.md
failures.jsonl缓存策略
缓存不是“同 URL 永久命中”,而是“抓取快照 + TTL + revalidate”。
Cache Key
缓存 key 包含:
- 规范化 URL
render_modeselector- headers
user-agent
Cache Value
缓存内容包含:
- 抓取结果
fetched_atetaglast_modified
工作模式
default- 未过期:直接返回缓存
- 已过期:带
If-None-Match/If-Modified-Since重验 304:刷新缓存时间并返回旧结果
refresh- 忽略旧缓存,强制重新抓取
only-if-cached- 只读缓存,不发网络请求;无缓存则报错
Browser Worker
动态页面抓取通过独立 Playwright worker 实现。
安装依赖:
cd browser-worker
npm install
npx playwright install chromium启动:
cd browser-worker
node src/server.js默认监听:
http://127.0.0.1:4000CLI 使用 browser 模式:
web-fetch \
--config web-fetch.example.toml \
https://example.com \
--render browserServer 用法
启动最小可用 server:
web-fetch-server --config web-fetch.example.toml健康检查:
curl http://127.0.0.1:8080/health返回格式:
{
"status": "ok",
"version": "0.0.1",
"server": {
"request_timeout_ms": 18000,
"batch_request_timeout_ms": 30000,
"max_batch_size": 32,
"max_batch_concurrency": 8
},
"llm_providers": [
{
"name": "primary",
"state": "healthy",
"consecutive_failures": 0,
"consecutive_slow": 0,
"last_latency_ms": 412,
"last_error": null,
"opened_until_epoch_ms": null
}
]
}单条抓取接口:
curl -X POST http://127.0.0.1:8080/fetch \
-H 'content-type: application/json' \
-d '{
"url": "example.com",
"render_mode": "auto",
"scheme_policy": "auto",
"selector": null,
"timeout_ms": 15000,
"headers": {},
"output_format": "json",
"prefer_cache": true,
"cache_mode": "default",
"cache_ttl_secs": 1800,
"cache_dir": ".web-fetch-cache",
"max_body_size": 5242880,
"retries": 0,
"user_agent": null
}'如果请求体中仍保留默认值,server 会用配置文件中的 request_defaults 补齐。
返回格式:
{
"request_url": "example.com",
"resolved_url": "https://example.com/",
"final_url": "https://example.com/",
"status": "ok",
"trace_id": "wf-1743991200000-1",
"metadata": {
"title": "Example Domain",
"byline": null,
"published_at": null,
"lang": "en",
"site_name": "example.com",
"status_code": 200,
"content_type": "text/html",
"fetched_at": "2026-04-07T10:00:00Z"
},
"markdown": "# Example Domain\n\nExample body",
"text": "Example Domain Example body",
"blocks": [
{ "type": "heading", "level": 1, "text": "Example Domain" },
{ "type": "paragraph", "text": "Example body" }
],
"content_hash": "sha256:...",
"diagnostics": {
"render_mode_used": "http",
"scheme_used": "https",
"fallbacks": [],
"warnings": [],
"timings_ms": { "fetch": 120, "parse_transform": 18, "server_total": 145 },
"cache_hit": false
},
"error": null
}批量抓取接口:
curl -X POST http://127.0.0.1:8080/fetch/batch \
-H 'content-type: application/json' \
-d '{
"requests": [
{
"url": "example.com",
"render_mode": "auto",
"scheme_policy": "auto",
"selector": null,
"timeout_ms": 15000,
"headers": {},
"output_format": "json",
"prefer_cache": true,
"cache_mode": "default",
"cache_ttl_secs": 1800,
"cache_dir": ".web-fetch-cache",
"max_body_size": 5242880,
"retries": 0,
"user_agent": null
},
{
"url": "python.org",
"render_mode": "auto",
"scheme_policy": "auto",
"selector": null,
"timeout_ms": 15000,
"headers": {},
"output_format": "json",
"prefer_cache": true,
"cache_mode": "default",
"cache_ttl_secs": 1800,
"cache_dir": ".web-fetch-cache",
"max_body_size": 5242880,
"retries": 0,
"user_agent": null
}
],
"concurrency": 8
}'返回结构包含:
resultsrequestedsucceededfailed
返回格式:
{
"trace_id": "wf-1743991200000-2",
"results": [
{ "status": "ok", "request_url": "example.com", "error": null },
{
"status": "error",
"request_url": "bad url",
"error": {
"code": "invalid_url",
"message": "URL must not be empty"
}
}
],
"requested": 2,
"succeeded": 1,
"failed": 1,
"timings_ms": {
"server_total": 380
}
}Trace ID 与日志
server 会为每个 /fetch 和 /fetch/batch 请求生成一个 trace_id:
- 响应头:
x-trace-id - 单条响应体:
trace_id - 批量响应体:
trace_id
可用这个值关联服务端结构化日志。
如需 JSON 日志:
WEB_FETCH_SERVER_LOG_JSON=1 web-fetch-server --config web-fetch.example.tomlserver 默认还会限制:
request_timeout_ms = 18000batch_request_timeout_ms = 30000max_batch_size = 32max_batch_concurrency = 8
用于保护延迟与吞吐稳定性。超时后在途 future 会被 drop,从而触发协作式取消。
测试
默认测试:
cargo test真实网页 live suite
- 国外样本:100 个
- 中国样本:100 个
- browser 样本:20 个
运行 HTTP/auto live suite:
cargo test -p web-fetch-core live_fetch_suite_covers_global_and_cn_cases -- --ignored --nocapture运行 browser suite:
BROWSER_WORKER_URL=http://127.0.0.1:4000 \
cargo test -p web-fetch-core live_browser_suite_covers_browser_cases -- --ignored --nocapture导出 live 报告:
WEB_FETCH_LIVE_REPORT_DIR=.reports/live \
cargo test -p web-fetch-core live_fetch_suite_covers_global_and_cn_cases -- --ignored --nocapture文档
docs/README.mddocs/v0.0.1-requirements.mddocs/v0.0.1-design.mddocs/v0.0.1-implementation.mddocs/v0.0.1-testing.mdRELEASING.md
路线
v0.0.1:单机 CLI + 最小 server + 配置化 LLM 抽取v0.0.x:抽取质量、缓存、稳定性增强v0.1.x:更完整服务化、任务化与可观测性
