npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-Modified revalidate
  • 支持批量输出到 stdout / file / jsonl / files
  • 支持最小可用 server:GET /healthPOST /fetchPOST /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 --help

npm 安装会:

  • 自动按平台安装对应预编译二进制包
  • 同时提供 web-fetchweb-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-fetchweb-fetch-server
  • 初始化默认配置到 ~/.config/web-fetch/web-fetch.toml
  • 若默认配置已存在,则保留原文件不覆盖

可选环境变量:

  • WEB_FETCH_VERSION=v0.0.1
  • WEB_FETCH_REF=main
  • WEB_FETCH_INSTALL_DIR=$HOME/.local/bin
  • WEB_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 sh

Windows PowerShell

irm https://git.neuxnet.com/cli/web-fetch-cli/-/raw/main/scripts/install.ps1 | iex

安装后可验证:

web-fetch --version
web-fetch-server --help

npm 打包

发布 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

发布流程:

  1. 各平台 runner 分别构建并打包平台 npm 包
  2. 汇总 tarball
  3. 先发布平台包
  4. 最后发布主包 @neuxmind/web-fetch

当前 GitLab CI 配置文件:

.gitlab-ci.yml

需要准备:

  • GitLab CI/CD 变量:NPM_TOKEN
  • 可选 GitLab CI/CD 变量:NPM_DIST_TAG
  • 可用 runner tags:
    • macos-arm64
    • macos-x64
    • linux-arm64
    • linux-x64
    • windows-arm64
    • windows-x64

发布前会校验:

  • CI_COMMIT_TAG 去掉前缀 v
  • 必须等于根目录 package.json 中的 version

创建并推送 tag:

git tag v0.0.1
git push origin v0.0.1

CLI 用法

最简单示例

输出 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"

抽取器会:

  1. 先用本地 heuristic 从 HTML 中抽取候选正文块
  2. 把候选正文块转换成受 max_input_chars 约束的 candidate_markdown
  3. 准备 candidate_markdown 和 metadata hints
  4. providers 顺序调用 OpenAI-compatible /chat/completions
  5. 单个 provider 按配置重试
  6. 单个 provider 的最大实际重试次数会被限制在 3
  7. 当前 provider 失败后自动降级到下一个 provider
  8. 全部 provider 失败时,如果 fallback_to_heuristic = true,则回退启发式抽取

为了控制速度和 token 成本,LLM 不再直接处理原始 HTML;它只处理本地抽取出来的候选 Markdown。

3 秒处理预算

为了服务端延迟可控,默认按 processing_timeout_ms = 3000 执行。

含义是:

  • 网页 HTML / browser DOM 一旦拿到
  • 后续正文抽取、LLM 调用、markdown 转换等处理
  • 默认必须在 3000ms 内结束

如果超时:

  • fallback_to_heuristic = true:快速降级到 heuristic
  • fallback_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 配置项

  • name
  • base_url
  • model
  • api_keyapi_key_env
  • retries
  • timeout_ms
  • headers
  • extra_body

provider 健康度与自动降级

当前实现会在进程内维护 provider 运行时健康度:

  • 连续失败达到 circuit_breaker_threshold 后,provider 会熔断 circuit_breaker_open_ms
  • 连续慢请求达到 slow_request_degrade_threshold 后,provider 会被标记为 degraded
  • 后续请求会优先尝试健康 provider,再尝试 degraded provider

相关配置位于 [extraction.llm]

  • circuit_breaker_threshold
  • circuit_breaker_open_ms
  • slow_request_threshold_ms
  • slow_request_degrade_threshold

返回格式要求

上游 LLM 需要返回严格 JSON,字段为:

  • title
  • byline
  • published_at
  • lang
  • site_name
  • markdown

为增强 OpenAI-compatible provider 兼容性,当前实现也会接受常见正文别名:

  • content
  • text
  • body

输出逻辑

CLI 支持 4 种输出模式:

  • stdout
  • file
  • jsonl
  • files

默认 --output-mode auto

stdout

web-fetch https://example.com --pretty

单文件

web-fetch https://example.com --output out/result.json --pretty

JSONL

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_mode
  • selector
  • headers
  • user-agent

Cache Value

缓存内容包含:

  • 抓取结果
  • fetched_at
  • etag
  • last_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:4000

CLI 使用 browser 模式:

web-fetch \
  --config web-fetch.example.toml \
  https://example.com \
  --render browser

Server 用法

启动最小可用 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
  }'

返回结构包含:

  • results
  • requested
  • succeeded
  • failed

返回格式:

{
  "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.toml

server 默认还会限制:

  • request_timeout_ms = 18000
  • batch_request_timeout_ms = 30000
  • max_batch_size = 32
  • max_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.md
  • docs/v0.0.1-requirements.md
  • docs/v0.0.1-design.md
  • docs/v0.0.1-implementation.md
  • docs/v0.0.1-testing.md
  • RELEASING.md

路线

  • v0.0.1:单机 CLI + 最小 server + 配置化 LLM 抽取
  • v0.0.x:抽取质量、缓存、稳定性增强
  • v0.1.x:更完整服务化、任务化与可观测性