关于 Fitter
Fitter 是一个将网站或 API 转换为结构化 JSON 的工具,数据获取与提取通过声明式的 JSON/YAML 配置完成。其 MCP 服务 Fitter MCP 让 Claude Code、Claude Desktop 等客户端可以编写、校验并在本地运行这些配置。配置为纯数据,便于审查、保存和复用。
主要功能
- 01以 JSON/YAML 声明式描述数据抓取与提取
- 02通过 MCP 工具校验并执行配置
- 03支持 HTTP、无头浏览器、文件和静态值来源
- 04支持 stdio 与可远程部署的 HTTP 传输
- 05提供 Prometheus 指标与 Docker 镜像
适用场景
- 01让模型根据自然语言需求生成并运行抓取配置
- 02将已验证的配置转为定时任务或服务配置
- 03抓取没有 API 的页面并用 JSON API 补充字段
MCP 相关
Fitter MCP 是 MCP 服务,向 MCP 客户端暴露 fitter_run、fitter_validate_config 和 fitter_inspect_url 等工具,并提供配置参考资源。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
Fitter — 面向 AI Agent 的 Web 数据
Fitter 可将任何网站或 API 以声明式方式转换为结构化 JSON。 只需一个 JSON/YAML 配置,就能描述数据所在位置(HTTP 请求、无头浏览器、文件、静态值)以及要提取的内容(JSON 路径、CSS 选择器、XPath)。无需编写代码,也不必维护脆弱的抓取脚本。
🚀 在浏览器中试用 — 通过 WebAssembly 编译的真实引擎:包含实时示例、可视化配置构建器,无需安装。
由于配置是纯数据,LLM 可以编写配置。内置的 MCP 服务可让 Claude Code、Claude Desktop 或任何 MCP 客户端按需在你的机器上编写并运行抓取工作流:
“获取 HackerNews 排名前 5 的故事及其标题和分数” → 模型编写 fitter 配置、验证配置、在本地运行,并返回整洁的 JSON。
一个引擎,五种使用方式:
| 🤖 Fitter MCP | MCP 服务,可让 Claude Code、Claude Desktop 和任何 MCP 客户端使用 fitter |
| 🧠 Fitter Agent | AI 驱动的命令行工具:自然语言 → 配置 → 执行结果 |
| 🖥 Fitter CLI | 在本地运行配置,用于测试、调试或日常使用 |
| 📦 Fitter Lib | 将引擎嵌入你自己的 Go 程序 |
| ⚙️ Fitter | 长时间运行的服务模式,支持调度和通知 |
为什么 AI Agent 适合使用 fitter?
- 声明式且可审计 — Agent 生成的配置可供你阅读、保存和重新运行,而不是用完即弃的代码
- 本地优先 — 所有数据获取都在你的机器上进行;无需第三方抓取 API、密钥,也没有按请求计费
- 功能齐全 — 单个静态二进制文件中包含 HTTP 客户端、无头浏览器(Playwright/Chromium/Docker)、JSON/HTML/XML/XPath/PDF 解析、分页、缓存引用和主机速率限制
- 可复用 — Agent 今天编写的内容,明天就能用作 cron 任务或服务配置
如何使用 Fitter_MCP
Fitter MCP 是一个 Model Context Protocol 服务(stdio 传输),可让任何 MCP 客户端(Claude Code、Claude Desktop、IDE 助手、自定义 Agent)运行 Fitter 配置并获取结构化 JSON。
快速开始(Claude Desktop — 一键安装)
从发布页面下载 fitter-mcp-<os>-<arch>.mcpb 并打开,Claude Desktop 会自动安装该服务。
快速开始(Claude Code)
# 1. get the binary: download fitter_mcp_<version>-<os>-<arch> from the release page
# https://github.com/PxyUp/fitter/releases — or build it from source:
go build -o fitter_mcp ./cmd/mcp
# 2. register it once, available in every project
claude mcp add fitter -s user -- "$(pwd)/fitter_mcp"
然后直接询问:
使用 fitter 获取 HackerNews 排名前 5 的故事及其标题和分数
模型会调用 fitter_config_reference 编写配置,可选择使用 fitter_validate_config 检查配置,然后通过 fitter_run 执行 — 所有数据获取都在你的机器上本地进行。如需使用现成的工作流,请尝试 examples/config_morning_briefing.json:
使用 fitter 运行 examples/config_morning_briefing.json 并给我简报
在 Claude Desktop 中注册
{
"mcpServers": {
"fitter": {
"command": "/path/to/fitter_mcp"
}
}
}
浏览器支持(Playwright)
.mcpb 捆绑包和原生二进制文件不包含浏览器:HTTP、静态和文件连接器开箱即用,但浏览器配置(playwright 连接器)需要 Playwright 的浏览器。可以通过以下几种方式获取:
- 首次使用时(原生二进制文件 /
.mcpb): 在 playwright 连接器中设置"install": true— fitter 会在首次使用时下载与其内置playwright-go版本匹配的驱动程序和浏览器(仅需一次,之后会缓存),因此无需单独安装。 - 提前安装(原生环境,可选): 若要避免首次运行时下载,请提前安装浏览器,并使用 fitter 构建时所依据的相同
playwright-go版本(查看go.mod,当前为v0.6100.0):
版本必须与go run github.com/mxschmitt/playwright-go/cmd/[email protected] install # Linux: append --with-deps to also install the required OS librariesgo.mod完全一致 —playwright-go无法使用不匹配的驱动程序运行。然后运行配置时无需设置"install": true。 - Docker: 使用
ghcr.io/pxyup/fitter-mcp:playwright镜像,其中已预装 Chromium、Firefox 和 WebKit(无需设置"install": true)。
工具
| 工具 | 描述 |
|---|---|
fitter_run | 运行以内联方式传入的 Fitter 配置(JSON 或 YAML 字符串),并将提取的数据作为 JSON 返回。接受可选的 input 值,可在配置中通过 {{{FromInput=.}}} / {{{FromInput=json.path}}} 使用 |
fitter_run_file | 与 fitter_run 相同,但从本地 .json/.yaml 文件读取配置 |
fitter_run_url | 与 fitter_run 相同,但从 HTTP(S) URL 下载配置,例如原始 GitHub 链接 |
fitter_inspect_url | 获取 URL 并返回精简的结构大纲和候选选择器/路径(JSON 使用 gjson 路径;HTML 使用重复元素/列表行选择器),让模型能一次性编写出配置,而不是猜测选择器并得到 null。可检测客户端渲染的 SPA,并可在无头浏览器中对其进行 render。只读 — 不会提取数据 |
fitter_validate_config | 不执行配置,只验证配置(结构、response_type、连接器数据源、模型)。适用于迭代调整配置 |
fitter_config_reference | 返回完整配置格式的精简参考(连接器、解析器、模型/字段架构、占位符、通知器、引用、限制),并附带可运行示例,让模型无需外部文档即可编写配置 |
支持资源的客户端也可通过 MCP 资源 fitter://config-reference 获取参考资料。
配置格式与 Fitter_CLI 完全相同:顶层对象包含 item(必填)、limits 和 references。通知器 也可用(结果还会额外推送到 http/telegram/redis/file/console);trigger_config 和 http_server 仅用于服务模式,在 MCP 调用中会被忽略。
远程 / 托管模式(可流式传输的 HTTP)
默认情况下,fitter_mcp 使用 stdio 通信。传入 --http 即可改为提供可流式传输的 HTTP 传输 — 适用于团队共享服务、容器或任何远程部署:
# serve MCP at http://<host>:8080/mcp (health probe at /healthz)
FITTER_MCP_AUTH_TOKEN=my-secret fitter_mcp --http :8080
# register the remote endpoint in Claude Code
claude mcp add --transport http fitter http://localhost:8080/mcp --header "Authorization: Bearer my-secret"
--http <addr>(环境变量FITTER_MCP_HTTP_ADDR)— 监听地址;为空时使用 stdio 模式FITTER_MCP_AUTH_TOKEN— 设置后,每个/mcp请求都必须发送Authorization: Bearer <token>;未设置时,端点不需要身份验证,因此请绑定到 localhost 或将其置于代理之后--stateless(环境变量FITTER_MCP_STATELESS=true)— 不保留会话状态,因此副本可置于负载均衡器之后,无需会话粘滞--metrics-addr <addr>(环境变量FITTER_MCP_METRICS_ADDR)— 在单独的地址上提供 Prometheus 指标(例如127.0.0.1:9091);stdio 模式下也可用;为空时禁用。提供按方法统计的 MCP 请求数、按工具和结果统计的工具调用时长,以及按主机、方法和状态统计的出站 HTTP 请求时长。该端点不需要身份验证 — 请将其绑定到 localhost 或私有接口
服务器会在收到 SIGINT/SIGTERM 时平稳关闭。
Docker
每次发布都会提供精简的多架构镜像(linux/amd64 + linux/arm64):
# hosted HTTP mode
docker run --rm -p 8080:8080 \
-e FITTER_MCP_HTTP_ADDR=:8080 \
-e FITTER_MCP_AUTH_TOKEN=my-secret \
ghcr.io/pxyup/fitter-mcp:latest
# or stdio mode, spawned by the MCP client
claude mcp add fitter -s user -- docker run --rm -i ghcr.io/pxyup/fitter-mcp:latest
精简镜像仅包含 fitter 二进制文件和 CA 证书:服务/静态/文件连接器可用,浏览器连接器(chromium/docker/playwright)不可用。
对于基于浏览器的配置,请使用 playwright 变体,其中包含 Playwright、Chromium、Firefox 和 WebKit(与构建 fitter 时所用的 playwright-go 版本匹配,因此配置中无需设置 "install": true):
docker run --rm -i ghcr.io/pxyup/fitter-mcp:playwright # stdio mode
# per-release tag: ghcr.io/pxyup/fitter-mcp:vX.Y.Z-playwright
该镜像基于 Dockerfile.mcp-playwright 构建;使用 --build-arg PLAYWRIGHT_BROWSERS=chromium 构建可获得更小的仅含 Chromium 的镜像。
Docker 中的 OAuth2 账号
两个镜像都包含 fitter_cli,因此可以在容器中执行一次性 OAuth2 登录。将令牌存储在挂载到 /tokens 的卷中(镜像中已预先创建且可写),并与 MCP 服务共享:
# one-time login, device flow: no ports needed — open the printed url on any device
docker run --rm -it -v fitter-tokens:/tokens --entrypoint fitter_cli \
ghcr.io/pxyup/fitter-mcp:latest \
auth --provider github --client-id <ID> --client-secret <SECRET> --token-file /tokens/github.json
# or browser flow (device flow not enabled for the app): publish the callback port and
# bind on 0.0.0.0 so the published port reaches the listener; the browser still visits 127.0.0.1
docker run --rm -it -p 8988:8988 -e FITTER_AUTH_LISTEN=0.0.0.0 \
-v fitter-tokens:/tokens --entrypoint fitter_cli ghcr.io/pxyup/fitter-mcp:latest \
auth --provider github --client-id <ID> --client-secret <SECRET> --token-file /tokens/github.json
# then run the MCP server with the same volume; configs reference "token_file": "/tokens/github.json"
# stdio mode (spawned by the MCP client, no port):
docker run --rm -i -v fitter-tokens:/tokens ghcr.io/pxyup/fitter-mcp:latest
# hosted HTTP mode (MCP endpoint on 8080, like the run examples above):
docker run --rm -p 8080:8080 -v fitter-tokens:/tokens \
-e FITTER_MCP_HTTP_ADDR=:8080 \
-e FITTER_MCP_AUTH_TOKEN=my-secret \
ghcr.io/pxyup/fitter-mcp:latest
注意:8988 仅用于一次性浏览器流程登录;MCP 服务本身在 stdio 模式下无需端口,在托管 HTTP 模式下仅需 8080。
Docker 中已登录的浏览器会话
浏览器会话需要 playwright 镜像(精简镜像不含浏览器)。一次性有头登录需要显示设备,因此请在主机上运行,然后将会话目录绑定挂载到容器中(镜像已预先创建可写的 /sessions):
# on the host: log in once, save the session
fitter_cli browser-login --url https://example.com/login --storage-state ~/.fitter/sessions/example.json
# run the MCP server with the sessions dir mounted; configs reference "storage_state_file": "/sessions/example.json"
docker run --rm -i -v ~/.fitter/sessions:/sessions ghcr.io/pxyup/fitter-mcp:playwright
请使用绑定挂载(而非具名卷):容器每次运行后都会写回更新后的 Cookie,因此主机上的副本始终是最新的,可随时通过 browser-login 延长会话。
服务必须能够写入该卷:每次刷新时都会写回轮换后的刷新令牌。
环境变量
- FITTER_PLUGINS - string[""] - 插件文件夹的路径,与 Fitter/Fitter_CLI 的
--plugins标志相同 - FITTER_MCP_HTTP_ADDR - string[""] - 远程模式的监听地址,与
--http相同 - FITTER_MCP_AUTH_TOKEN - string[""] - 用于保护 HTTP 端点的持有者令牌
- FITTER_MCP_STATELESS - bool[false] - 无状态 HTTP 传输,与
--stateless相同 - FITTER_MCP_METRICS_ADDR - string[""] - Prometheus 指标监听地址,与
--metrics-addr相同
示例
展示主要模式的完整且经过测试的配置。所有配置都可通过 Fitter_MCP(fitter_run_file)、Fitter_CLI 或库直接运行,无需修改——更多示例见 examples/。
抓取没有 API 的页面,并从有 API 的页面补充数据
GitHub 趋势没有官方 API——抓取 HTML 获取仓库标识(html_attribute 会读取 href),然后使用 {PL} 将每个标识传入 GitHub REST API:
examples/config_github_trending.json
{
"item": {
"connector_config": {
"response_type": "HTML",
"url": "https://github.com/trending",
"server_config": { "method": "GET", "headers": { "User-Agent": "Mozilla/5.0 (fitter demo)" } }
},
"model": {
"array_config": {
"root_path": "article.Box-row h2 a",
"length_limit": 5,
"item_config": {
"field": {
"type": "string",
"html_attribute": "href",
"generated": { "model": {
"type": "object",
"connector_config": {
"response_type": "json",
"url": "https://api.github.com/repos{PL}",
"server_config": { "method": "GET", "headers": { "User-Agent": "fitter-demo" } },
"null_on_error": true
},
"model": { "object_config": { "fields": {
"repo": { "base_field": { "type": "string", "path": "full_name" } },
"stars": { "base_field": { "type": "int", "path": "stargazers_count" } },
"language": { "base_field": { "type": "string", "path": "language" } }
} } }
} }
}
}
}
}
},
"limits": { "host_request_limiter": { "api.github.com": 2 } }
}
[{"repo": "block/buzz", "stars": 6214, "language": "Rust"}, {"repo": "koala73/worldmonitor", "stars": 71179, "language": "TypeScript"}]
使用表达式按 JSON 字段联接
当数组项是对象时,联接键位于对象内部——使用 {{{FromExp=...}}} 将其取出(通过作用于当前项的 fRes 使用 expr-lang)。搜索图书 → 作者详情,搜索查询通过 input 提供:
examples/config_book_authors.json
"url": "https://openlibrary.org/authors/{{{FromExp=fromJSON(fRes).author_key[0]}}}.json"
./fitter_cli --path=examples/config_book_authors.json --input=dune
[{"title": "Dune", "year": 1965, "author": {"name": "Frank Herbert", "born": "8 October 1920", "died": "11 February 1986"}}]
将结果写入本地文件
file_storage 生成字段会将字段转换为写入操作:将排名前 5 的加密货币追加到 CSV,每个项目占一行。裸 {{{json.path}}} 占位符会读取当前项目;{HUMAN_INDEX} 会标记从 1 开始的排名(项目会并行处理,因此追加顺序取决于完成顺序——请按排名列排序):
examples/config_crypto_csv.json
"file_storage": {
"content": "{HUMAN_INDEX},{{{name}}},{{{current_price}}},{{{price_change_percentage_24h}}}\n",
"file_name": "coins.csv",
"path": "/tmp/fitter-report",
"append": true
}
$ sort -n /tmp/fitter-report/coins.csv
1,Bitcoin,64778,-2.3
2,Ethereum,1881.01,-3.4
3,Tether,0.999265,0
从 PDF 中提取文本
response_type: "pdf" 会将获取到的任何 PDF 转换为 JSON 文档——{"text": "...", "pages": ["..."], "total_pages": N}——因此常规 JSON 路径(text、pages.0)和表达式都可用于处理。以下示例展示 Bitcoin 白皮书的页数以及经过裁剪的简介:
{
"item": {
"connector_config": {
"response_type": "pdf",
"url": "https://bitcoin.org/bitcoin.pdf",
"server_config": { "method": "GET" }
},
"model": {
"object_config": {
"fields": {
"total_pages": { "base_field": { "type": "int", "path": "total_pages" } },
"intro": {
"base_field": {
"type": "string",
"path": "pages.0",
"generated": {
"calculated": {
"type": "string",
"expression": "trim(fRes[:100]) + \"...\""
}
}
}
}
}
}
}
}
}
{"intro": "Bitcoin: A Peer-to-Peer Electronic Cash SystemSatoshi [email protected]...", "total_pages": 9}
信息收集方式
- Server - 解析某些 API 或 HTTP 请求的响应(使用 http.Client)
- Browser - 使用 chromium + docker + playwright/cypress 模拟真实浏览器并获取 DOM 信息
- Static - 将静态字符串解析为数据
可解析的格式
- JSON - 解析 JSON 以获取特定信息
- XML - 解析 XML 树以获取特定信息
- HTML - 解析 DOM 树以获取特定信息
- XPath - 使用 XPath 解析 DOM 树以获取特定信息
- PDF - 从 PDF 文档中提取文本;内容以 JSON 形式提供:
{"text": "...", "pages": ["..."], "total_pages": N},因此可使用text或pages.0等常规 JSON 路径
作为库使用
go get github.com/PxyUp/fitter
package main
import (
"fmt"
"github.com/PxyUp/fitter/lib"
"github.com/PxyUp/fitter/pkg/config"
"log"
"net/http"
)
func main() {
res, err := lib.Parse(&config.Item{
ConnectorConfig: &config.ConnectorConfig{
ResponseType: config.Json,
Url: "https://random-data-api.com/api/appliance/random_appliance",
ServerConfig: &config.ServerConnectorConfig{
Method: http.MethodGet,
},
},
Model: &config.Model{
ObjectConfig: &config.ObjectConfig{
Fields: map[string]*config.Field{
"my_id": {
BaseField: &config.BaseField{
Type: config.Int,
Path: "id",
},
},
"generated_id": {
BaseField: &config.BaseField{
Generated: &config.GeneratedFieldConfig{
UUID: &config.UUIDGeneratedFieldConfig{},
},
},
},
"generated_array": {
ArrayConfig: &config.ArrayConfig{
RootPath: "@this|@keys",
ItemConfig: &config.ObjectConfig{
Field: &config.BaseField{
Type: config.String,
},
},
},
},
},
},
},
}, nil, nil, nil, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(res.ToJson())
}
输出:
{
"generated_array": ["id","uid","brand","equipment"],
"my_id": 6000,
"generated_id": "26b08b73-2f2e-444d-bcf2-dac77ac3130e"
}
使用 lib.ParseCtx(ctx, ...) 传入 context.Context:取消该上下文会中止正在进行的获取操作(HTTP 请求、无头浏览器、docker 容器),并在整个调用过程中应用截止时间。lib.Parse 等同于 lib.ParseCtx(context.Background(), ...)。
如何使用 Fitter
或在本地使用:
go run cmd/fitter/main.go --path=./examples/config_api.json
参数
- --path - string[""] - Fitter 配置文件的路径
- --url - string[""] - Fitter 配置的 URL
- --verbose - bool[false] - 启用日志记录
- --plugins - string[""] - Fitter 插件的路径
- --log-level - enum["info", "error", "debug", "fatal"] - 设置日志级别(仅在启用 verbose 时生效)
如何使用 Fitter_CLI
或在本地使用:
go run cmd/cli/main.go --path=./examples/cli/config_cli.json
参数
- --path - string[""] - Fitter_CLI 配置文件的路径
- --url - string[""] - Fitter_CLI 配置的 URL
- --copy - bool[false] - 将信息复制到剪贴板
- --pretty - bool[true] - 使结果更易读(也会影响复制结果)
- --verbose - bool[false] - 启用日志记录
- --omit-error-pretty - bool[false] - 如果 pretty 无效,则提供纯值
- --plugins - string[""] - Fitter 插件的路径
- --log-level - enum["info", "error", "debug", "fatal"] - 设置日志级别(仅在启用 verbose 时生效)
- --input - string[""] - 指定用于格式化的输入值。示例:
--input=\""124"\"--input=124--input='{"test": 5}'
./fitter_cli_${VERSION} --path=./examples/cli/config_cli.json --copy=true
fitter_cli auth — 连接 OAuth2 账户
一次性交互式登录,用于存储一个(刷新)令牌,供 oauth2 connector config 使用:
# device flow (default when the provider supports it): no callback, works headless
./fitter_cli_${VERSION} auth --provider github --client-id <ID> --client-secret <SECRET> --token-file ~/.fitter/tokens/github.json
# custom provider without preset
./fitter_cli_${VERSION} auth --auth-url https://.../authorize --token-url https://.../token --client-id <ID> --token-file ./token.json
参数:
- --provider - 预设的已知端点:
github|google|microsoft|gitlab|spotify - --client-id / --client-secret - OAuth2 应用凭据(某些设备授权流程无需密钥)
- --token-file - 接收的令牌的存储位置(权限为 0600);在
oauth2.token_file中引用相同路径 - --flow -
auto(如果可用则使用设备授权,否则使用浏览器)、device(访问网址并输入代码)或browser(使用 PKCE 的 localhost 回调,默认端口为 8988 — 将http://127.0.0.1:8988/callback注册为应用回调 URL) - --scopes - 以逗号分隔的作用域
- --auth-url/--token-url/--device-auth-url/--auth-style - 未预设提供方的端点覆盖项
- --port - int[8988] - 浏览器授权流程的回调端口(环境变量
FITTER_AUTH_PORT);使用默认值时,需在提供方注册的回调 URL 为http://127.0.0.1:8988/callback - --listen - 浏览器授权流程的绑定地址,默认为
127.0.0.1;在容器内设为0.0.0.0,使已发布的端口能够连接到监听器(环境变量FITTER_AUTH_LISTEN) - --redirect-url - 在提供方注册的回调 URL,当其与监听地址不同时使用,例如 Docker 端口映射(环境变量
FITTER_AUTH_REDIRECT_URL) - --no-browser - 仅打印授权 URL
在 Docker 中运行:请参阅 Docker 中的 OAuth2 账户。
登录后,命令会打印可直接使用的 oauth2 配置块。连接器会自动刷新访问令牌,并将轮换后的刷新令牌写回令牌文件,因此只需登录一次。
fitter_cli browser-login — 复用真实登录会话
对于没有 API/OAuth 的网站:在真实的(有头)浏览器窗口中手动登录一次——支持任何身份验证方案,包括密码、2FA、SSO 和验证码——然后保存会话,以便通过 storage_state_file 进行无头抓取:
./fitter_cli_${VERSION} browser-login --url https://example.com/login --storage-state ~/.fitter/sessions/example.json
# a browser window opens; log in, then press Enter in the terminal to save the session
参数:
- --url - 要打开的登录页面(必填)
- --storage-state - 会话的保存位置(cookies + localStorage,权限为 0600);在
playwright.storage_state_file中引用相同路径(必填) - --browser - 枚举值 ["Chromium", "FireFox", "WebKit"],默认值为 "Chromium";使用与抓取配置相同的值——网站可能会将会话绑定到浏览器指纹
- --install - bool[false] - 先安装 playwright 浏览器
- --indexeddb - bool[false] - 在快照中包含 IndexedDB(Firebase Auth 等)
重新运行命令时会先加载现有状态,因此你可以延长或刷新会话,而无需从头登录。抓取连接器也会在每次运行后写回刷新后的 cookies,只要定期使用,会话就能保持有效。需要显示器:在 Docker 中,请在主机上运行此命令并挂载文件——参见 Docker 中的浏览器会话。
示例:
- 服务器版本 HackerNews + Quotes + Guardian News - 使用 API + HTML + XPath 解析
- Chromium 版本 Guardian News + Quotes - 使用 HTML 解析 + 浏览器模拟
- Docker 版本 Docker version: Guardian News + Quotes - 使用 HTML 解析 + Docker 镜像中的浏览器
- Playwright 版本 Playwright version: Guardian News + Quotes - 使用 HTML 解析 + Playwright 框架中的浏览器
- Playwright 版本 Playwright version: England Cities + Weather - 使用 HTML + XPath 解析 + Playwright 框架中的浏览器
- JSON 版本 Generate pagination - 使用静态连接器生成分页数组
- 服务器版本 Get current time - 从 URL 获取时间并格式化
如何使用 Fitter_Agent
Fitter Agent 是一个由 AI 驱动的命令行工具,它使用 Claude 将自然语言请求转换为 Fitter 配置并自动执行。
或在本地运行:
export ANTHROPIC_API_KEY=<your-anthropic-api-key>
go run cmd/agent/main.go
参数
- --api-key - string[""] - Anthropic API 密钥。建议使用
ANTHROPIC_API_KEY环境变量,以免密钥进入 shell 历史记录 - --model - string["claude-opus-4-8"] - 要使用的 Claude 模型
- --effort - enum["low", "medium", "high", "xhigh", "max"] - 推理力度,默认值为 "high"。降低可使配置生成更快、成本更低;面对更难的提取任务时可提高
- --verbose - bool[false] - 启用日志记录
- --log-level - enum["info", "error", "debug", "fatal"] - 设置日志级别
- --plugins - string[""] - Fitter 插件的路径
- --chromium-limit - uint[0] - 限制并发 Chromium 实例数
- --docker-limit - uint[0] - 限制并发 Docker 容器数
- --playwright-limit - uint[0] - 限制并发 Playwright 实例数
工作原理
┌─────────────────────────────────────────────────────────────────┐
│ 1. User enters natural language request │
│ "Get top 5 HackerNews stories with titles and scores" │
│ ↓ │
│ 2. Claude returns a config in a schema-constrained response │
│ ↓ │
│ 3. Agent validates it; on failure the error is handed back │
│ to Claude to repair (up to 3 attempts) │
│ ↓ │
│ 4. Agent displays config and asks for confirmation │
│ ↓ │
│ 5. On confirmation, executes via lib.Parse() │
│ ↓ │
│ 6. Returns structured JSON result │
└─────────────────────────────────────────────────────────────────┘
优化配置
Agent 会保留对话,因此生成配置后,你只需说明要更改的内容,无需重述整个请求:
> Get top 3 HackerNews stories with titles and scores
refine> Only return 5 items and also include the article URL
使用 new 可忘记当前配置并开始新的会话。
交互式 REPL 命令
- help - 显示帮助信息
- new/reset - 忘记当前配置并重新开始
- clear - 清屏
- exit/quit/q - 退出 Agent
会话示例
$ export ANTHROPIC_API_KEY=sk-ant-...
$ ./fitter_agent
╔══════════════════════════════════════════════════════════════╗
║ Fitter Agent - AI-Powered Data Extraction ║
╚══════════════════════════════════════════════════════════════╝
Describe what you want to extract. Follow-up messages refine the
previous config. Type 'help' for commands.
> Get top 3 HackerNews stories with titles and scores
┌─ Generated Fitter Config ───────────────────────────────────────
{
"item": {
"connector_config": {
"response_type": "json",
"url": "https://hacker-news.firebaseio.com/v0/topstories.json",
"server_config": { "method": "GET" }
},
"model": {
"array_config": {
"root_path": "@this",
"length_limit": 3,
"item_config": {
"fields": {
"id": { "base_field": { "type": "int" } },
"story": {
"base_field": {
"type": "int",
"generated": {
"model": {
"type": "object",
"connector_config": {
"response_type": "json",
"url": "https://hacker-news.firebaseio.com/v0/item/{PL}.json",
"server_config": { "method": "GET" }
},
"model": {
"object_config": {
"fields": {
"title": { "base_field": { "type": "string", "path": "title" } },
"score": { "base_field": { "type": "int", "path": "score" } }
}
}
}
}
}
}
}
}
}
}
}
}
}
└──────────────────────────────────────────────────────────────────
Execute this config? [y/n]: y
┌─ Result ────────────────────────────────────────────────────────
[
{
"id": 46740029,
"story": { "title": "Show HN: Open-source project", "score": 161 }
},
{
"id": 46737630,
"story": { "title": "Interesting article", "score": 237 }
},
{
"id": 46735644,
"story": { "title": "New technology release", "score": 192 }
}
]
└──────────────────────────────────────────────────────────────────
> exit
Goodbye!
请求示例
| 请求 | 功能 |
|---|---|
Get Bitcoin price from CoinGecko API | 获取当前 BTC 价格 |
Scrape headlines from news.ycombinator.com with links | 使用 CSS 选择器抓取 HTML |
Get top 5 stories from HackerNews with titles | 嵌套 API 调用 |
Fetch weather data from wttr.in for London | 简单 API 提取 |
Scrape product names and prices from example.com | 网页抓取 |
支持的功能
该 Agent 可以生成以下类型的配置:
- JSON API - 使用 GET/POST 方法的 REST API
- HTML 抓取 - 基于 CSS 选择器提取
- XPath 抓取 - 基于 XPath 提取
- 嵌套 API 调用 - 获取列表中每个项目的详细信息
- 浏览器模拟 - 使用 Playwright 处理 JS 渲染的页面
- 格式化字段 - 带占位符的 URL 模板
- 数组限制 - 将结果限制为 N 个项目
配置
连接器
它是获取数据的方式
type ConnectorConfig struct {
ResponseType ParserType `json:"response_type" yaml:"response_type"`
Url string `json:"url" yaml:"url"`
Attempts uint32 `json:"attempts" yaml:"attempts"`
NullOnError bool `yaml:"null_on_error" json:"null_on_error"`
StaticConfig *StaticConnectorConfig `json:"static_config" yaml:"static_config"`
IntSequenceConfig *IntSequenceConnectorConfig `json:"int_sequence_config" yaml:"int_sequence_config"`
ServerConfig *ServerConnectorConfig `json:"server_config" yaml:"server_config"`
BrowserConfig *BrowserConnectorConfig `yaml:"browser_config" json:"browser_config"`
PluginConnectorConfig *PluginConnectorConfig `json:"plugin_connector_config" yaml:"plugin_connector_config"`
ReferenceConfig *ReferenceConnectorConfig `yaml:"reference_config" json:"reference_config"`
FileConfig *FileConnectorConfig `json:"file_config" yaml:"file_config"`
}
- NullOnError[false] - 如果设为 true,则忽略所有错误
- ResponseType - 枚举值["HTML", "json", "xpath", "XML", "pdf"] - 连接器返回的数据格式
- Attempts - 连接器获取数据时尝试的次数
- Url - 指定要请求的地址。注意:可以将父级值作为字符串注入
https://api.open-meteo.com/v1/forecast?latitude={{{latitude}}}&longitude={{{longitude}}}&hourly=temperature_2m&forecast_days=1
配置可以是以下类型之一:
- ServerConfig
- BrowserConfig
- StaticConfig
- PluginConnectorConfig
- ReferenceConfig
- IntSequenceConfig
- FileConfig
示例:
{
"response_type": "xpath",
"attempts": 3,
"url": "https://openweathermap.org/find?q={PL}",
"browser_config": {
"playwright": {
"timeout": 30,
"wait": 30,
"install": false,
"browser": "Chromium"
}
}
}
PluginConnectorConfig
连接器可以通过插件系统定义。使用该功能时,需要为 Fitter/Cli(插件所在位置)添加以下标志:
... --plugins=./examples/plugin
--plugins - 在指定文件夹中查找所有扩展名为 ".so" 的文件(不包括子目录)
type PluginConnectorConfig struct {
Name string `json:"name" yaml:"name"`
Config json.RawMessage `json:"config" yaml:"config"`
}
{
"name": "connector",
"config": {
"name": "Elon"
}
}
- Name - 插件名称
- Config - 插件的 json 配置
如何构建插件
构建插件
go build -buildmode=plugin -gcflags="all=-N -l" -o examples/plugin/connector.so examples/plugin/connector/connector.go
确保导出实现 pl.ConnectorPlugin 接口的 Plugin 变量
CLI 示例:
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_plugin.json#L5
插件示例:
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/PxyUp/fitter/pkg/config"
"github.com/PxyUp/fitter/pkg/logger"
"github.com/PxyUp/fitter/pkg/builder"
pl "github.com/PxyUp/fitter/pkg/plugins/plugin"
)
var (
_ pl.ConnectorPlugin = &plugin{}
Plugin plugin
)
type plugin struct {
log logger.Logger
Name string `json:"name" yaml:"name"`
}
func (pl *plugin) Get(ctx context.Context, parsedValue builder.Interfacable, index *uint32, input builder.Interfacable) ([]byte, error) {
return []byte(fmt.Sprintf(`{"name": "%s"}`, pl.Name)), nil
}
func (pl *plugin) SetConfig(cfg *config.PluginConnectorConfig, logger logger.Logger) {
pl.log = logger
if cfg.Config != nil {
err := json.Unmarshal(cfg.Config, pl)
if err != nil {
pl.log.Errorw("cant unmarshal plugin configuration", "error", err.Error())
return
}
}
}
ReferenceConnectorConfig
用于从引用中获取预取数据的连接器
type ReferenceConnectorConfig struct {
Name string `yaml:"name" json:"name"`
}
示例
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_ref.json#L66
- Name - references映射中的引用名称
IntSequenceConnectorConfig
改进版的静态连接器,可生成整数序列作为结果
type IntSequenceConnectorConfig struct {
Start int `json:"start" yaml:"start"`
End int `json:"end" yaml:"end"`
Step int `json:"step" yaml:"step"`
}
- Start[0] - 生成起点(包含)
- End[0] - 生成终点(不包含在最终结果中,与任何语言中的 range 类似)
- Step[1] - 序列间隔
示例
{
"start": 0,
"end": 2
// Generate [0, 1]
}
FileConnectorConfig
从指定文件获取数据的连接器类型
type FileConnectorConfig struct {
Path string `yaml:"path" json:"path"`
UseFormatting bool `yaml:"use_formatting" json:"use_formatting"`
}
StaticConnectorConfig
从指定字符串获取数据的连接器类型
type StaticConnectorConfig struct {
Value string `json:"value" yaml:"value"`
Raw json.RawMessage `json:"raw" yaml:"raw"`
}
示例:
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_static_connector.json#L5
{
"value": "[1,2,3,4,5]"
}
ServerConnectorConfig
使用 golang http.Client 获取数据的连接器类型(服务端请求,类似 curl)
type ServerConnectorConfig struct {
Method string `json:"method" yaml:"method"`
Headers map[string]string `yaml:"headers" json:"headers"`
Timeout uint32 `yaml:"timeout" json:"timeout"`
JsonRawBody json.RawMessage `json:"json_raw_body" yaml:"json_raw_body"`
Body string `yaml:"body" json:"body"`
ErrorOnStatus bool `json:"error_on_status" yaml:"error_on_status"`
Proxy *ProxyConfig `yaml:"proxy" json:"proxy"`
OAuth2 *OAuth2Config `yaml:"oauth2" json:"oauth2"`
}
- Method - 支持所有 HTTP 方法:GET、POST、PUT、DELETE、PATCH、OPTIONS、HEAD
- Headers - 预定义请求过程中使用的标头可注入到键/值中
- Timeout[sec] - 超时时间,默认 60 秒,也可使用指定值
- Body - 请求正文,解析后的值可注入
- JsonRawBody - JSON 格式的请求正文;值可注入
- ErrorOnStatus - 可选,默认为
false。设为true时,HTTP 响应状态>= 400会被视为获取错误(通过attempts/null_on_error处理),而不是解析错误正文,因此可以区分获取失败和结果确实为空。保持为false则会继续按原有行为解析返回的任何正文。 - Proxy - 为请求设置代理配置
- OAuth2 - 自动获取/刷新访问令牌,并将其作为
Authorization标头发送配置
默认情况下,请求会发送可识别的
User-Agent(fitter (+https://github.com/PxyUp/fitter));在Headers中设置自己的User-Agent即可覆盖它。
示例:
{
"method": "GET",
"proxy": {
"server": "http://localhost:8080",
"username": "pyx"
}
}
OAuth2 配置
在请求前自动获取访问令牌,并将其作为 Authorization 标头注入(覆盖通过 headers 设置的标头)。令牌会缓存在内存中,并在过期前刷新;收到 401 响应时,会丢弃缓存的令牌,并使用新令牌重试一次请求。
type OAuth2Config struct {
TokenUrl string `json:"token_url" yaml:"token_url"`
GrantType OAuth2GrantType `json:"grant_type" yaml:"grant_type"`
ClientId string `json:"client_id" yaml:"client_id"`
ClientSecret string `json:"client_secret" yaml:"client_secret"`
Scopes []string `json:"scopes" yaml:"scopes"`
RefreshToken string `json:"refresh_token" yaml:"refresh_token"`
EndpointParams map[string]string `json:"endpoint_params" yaml:"endpoint_params"`
AuthStyle string `json:"auth_style" yaml:"auth_style"`
TokenFile string `json:"token_file" yaml:"token_file"`
}
- TokenUrl - 令牌端点 URL。也支持格式化
- GrantType - 枚举值["client_credentials", "refresh_token"],默认为 "client_credentials"。对于用户已同意一次(Google、Microsoft 等)且你持有长期刷新令牌的 API,请使用 "refresh_token"
- ClientId/ClientSecret - 客户端凭据。也支持格式化,例如
{{{FromEnv=CLIENT_SECRET}}} - Scopes - 请求的作用域
- RefreshToken - "refresh_token" 授权类型必填。也支持格式化
- EndpointParams - 令牌端点的额外参数(例如 Auth0 的
audience),仅适用于 "client_credentials" 授权类型 - AuthStyle - 枚举值["", "header", "params"] - 客户端凭据传递到令牌端点的方式:基本认证标头或请求正文;留空表示自动检测
- TokenFile - 可选的令牌持久化路径(支持
~/),用于在多次运行之间保存令牌;存储的令牌优先于 RefreshToken,轮换后的刷新令牌会写回文件——使用 GitHub Apps 等采用一次性刷新令牌的提供方时必填。使用 fitter_cli auth 创建该文件
示例:
{
"method": "GET",
"oauth2": {
"token_url": "https://oauth2.googleapis.com/token",
"grant_type": "refresh_token",
"client_id": "{{{FromEnv=GOOGLE_CLIENT_ID}}}",
"client_secret": "{{{FromEnv=GOOGLE_CLIENT_SECRET}}}",
"refresh_token": "{{{FromEnv=GOOGLE_REFRESH_TOKEN}}}"
}
}
Proxy 配置
type ProxyConfig struct {
// Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example
// `http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128`
// is considered an HTTP proxy.
Server string `json:"server" yaml:"server"`
// Optional username to use if HTTP proxy requires authentication.
Username string `json:"username" yaml:"username"`
// Optional password to use if HTTP proxy requires authentication.
Password string `json:"password" yaml:"password"`
}
{
"server": "http://localhost:8080",
"username": "pyx"
}
环境变量
- FITTER_HTTP_WORKER - int[1000] - 默认并发 HTTP 工作线程数
BrowserConnectorConfig
模拟通过浏览器获取数据的连接器类型
type BrowserConnectorConfig struct {
Chromium *ChromiumConfig `json:"chromium" yaml:"chromium"`
Docker *DockerConfig `json:"docker" yaml:"docker"`
Playwright *PlaywrightConfig `json:"playwright" yaml:"playwright"`
}
配置可以是以下类型之一:
- Chromium - 使用本地安装的 Chromium 获取数据
- Docker - 使用 Docker 服务启动容器以获取数据
- Playwright - 使用 Playwright 框架获取数据
示例:
{
"docker": {
"wait": 10000,
"image": "docker.io/zenika/alpine-chrome:with-node",
"entry_point": "chromium-browser",
"purge": true
}
}
Chromium
使用本地安装的 Chromium 获取数据
type ChromiumConfig struct {
Path string `yaml:"path" json:"path"`
Timeout uint32 `yaml:"timeout" json:"timeout"`
Wait uint32 `yaml:"wait" json:"wait"`
Flags []string `yaml:"flags" json:"flags"`
}
- Path - Chromium 二进制文件的路径
- Timeout[sec] - Chromium 的执行超时时间
- Wait[msec] - 页面加载超时时间
- Flags - Chromium 标志,默认值:"--headless"、"--proxy-auto-detect"、"--temp-profile"、"--incognito"、"--disable-logging"、"--disable-extensions"、"--no-sandbox"
示例:
{
"path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"wait": 10000
}
Docker
使用 Docker 启动容器以获取数据
type DockerConfig struct {
Image string `yaml:"image" json:"image"`
EntryPoint string `json:"entry_point" yaml:"entry_point"`
Timeout uint32 `yaml:"timeout" json:"timeout"`
Wait uint32 `yaml:"wait" json:"wait"`
Flags []string `yaml:"flags" json:"flags"`
Purge bool `json:"purge" yaml:"purge"`
NoPull bool `yaml:"no_pull" json:"no_pull"`
PullTimeout uint32 `yaml:"pull_timeout" json:"pull_timeout"`
}
Docker 默认镜像:docker.io/zenika/alpine-chrome
- Image - Docker 注册表的镜像(提供注册表主机)
- EntryPoint - 将在容器内运行的命令
- Timeout[sec] - 运行容器的超时时间(不包括拉取镜像)
- Wait[msec] - 页面加载超时时间(仅适用于基于 Chromium 的容器)
- Flags - 运行容器的命令参数,基于 Chromium 时的默认值为:"--no-sandbox","--headless", "--proxy-auto-detect", "--temp-profile", "--incognito", "--disable-logging", "--disable-gpu"
- Purge - 工作完成后是否删除容器(类似 docker rm)
- NoPull - 阻止拉取镜像
- PullTimeout - 设置拉取容器的超时时间
环境变量
- DOCKER_HOST - string - (EnvOverrideHost),用于设置 Docker 服务的 URL。
- DOCKER_API_VERSION - string - (EnvOverrideAPIVersion),用于设置要使用的 API 版本;留空表示使用最新版本。
- DOCKER_CERT_PATH - string - (EnvOverrideCertPath),用于指定加载 TLS 证书(ca.pem、cert.pem、key.pem)的目录。
- DOCKER_TLS_VERIFY - bool - (EnvTLSVerify),用于启用或禁用 TLS 验证(默认关闭)
示例:
{
"wait": 10000,
"image": "docker.io/zenika/alpine-chrome:with-node",
"entry_point": "chromium-browser",
"purge": true
}
Playwright
通过 Playwright 框架运行浏览器
type PlaywrightConfig struct {
Browser PlaywrightBrowser `json:"browser" yaml:"browser"`
Install bool `yaml:"install" json:"install"`
Timeout uint32 `yaml:"timeout" json:"timeout"`
Wait uint32 `yaml:"wait" json:"wait"`
TypeOfWait *playwright.WaitUntilState `json:"type_of_wait" yaml:"type_of_wait"`
PreRunScript string `json:"pre_run_script" yaml:"pre_run_script"`
PostRunScript string `json:"post_run_script" yaml:"post_run_script"`
Stealth bool `json:"stealth" yaml:"stealth"`
StorageStateFile string `json:"storage_state_file" yaml:"storage_state_file"`
IndexedDB bool `json:"indexed_db" yaml:"indexed_db"`
Proxy *ProxyConfig `yaml:"proxy" json:"proxy"`
}
- Browser - enum["Chromium", "FireFox", "WebKit"] - 要使用的浏览器
- Install - 是否安装浏览器(首次使用时下载与内置
playwright-go版本匹配的驱动和浏览器;使用预装了它们的ghcr.io/pxyup/fitter-mcp:playwright镜像时则不需要) - Timeout[sec] - 运行 Playwright 的超时时间
- Wait[sec] - 页面加载超时时间
- TypeOfWait - enum["load", "domcontentloaded", "networkidle", "commit"],要等待的页面状态,默认值为 "load"
- PreRunScript[""] - 通过 AddInitScript 注入并在任何页面脚本运行前执行的脚本(在创建文档时、导航完成前)。适用于修补环境(覆盖 navigator、模拟 API)。无法访问已加载的 DOM。也支持占位符 {PL}
- PostRunScript[""] - 页面加载后、读取页面内容前执行的脚本。适用于 DOM 交互(点击、滚动)。也支持占位符 {PL}
- Stealth[false] - 添加用于尝试绕过机器人防护的脚本
- StorageStateFile[""] - Playwright 存储状态 JSON(Cookie + localStorage)的路径(支持
~/):导航前加载到浏览器上下文中,每次运行后写回,以保持刷新的会话有效。这样无头运行也能复用真实登录状态——使用 fitter_cli browser-login 创建一次文件。登录和抓取时使用相同的browser:网站可能会将会话绑定到浏览器指纹。也支持格式化 - IndexedDB[false] - 将 IndexedDB 包含在持久化存储状态中(一些 SPA,例如 Firebase Auth,会在其中保存令牌)
- Proxy - 为请求配置代理配置
示例
{
"timeout": 30,
"wait": 30,
"install": false,
"browser": "Chromium"
}
模型
通过模型定义抓取结果
type Model struct {
ObjectConfig *ObjectConfig `yaml:"object_config" json:"object_config"`
ArrayConfig *ArrayConfig `json:"array_config" yaml:"array_config"`
BaseField *BaseField `json:"base_field" yaml:"base_field"`
IsArray bool `json:"is_array" yaml:"is_array"`
}
配置可以是以下之一:
- ObjectConfig - 对象格式的配置
- ArrayConfig - 数组格式的配置
- BaseField - 单个/生成字段的配置
- IsArray - bool[false] - 强制将字段指定为数组(适用于包含 BaseField 的模型字段)
示例:
{
"object_config": {}
}
ObjectConfig
对象和字段的配置
type ObjectConfig struct {
Fields map[string]*Field `json:"fields" yaml:"fields"`
Field *BaseField `json:"field" yaml:"field"`
ArrayConfig *ArrayConfig `json:"array_config" yaml:"array_config"`
Condition string `json:"condition" yaml:"condition"`
}
- Condition - 可选的 condition 表达式,在解析之前针对源节点进行求值;如果结果为 false,则从父对象中省略整个对象(完全不解析字段)
配置可以是以下任意一种:
- Fields - 每个字段定义的映射;键为字段名称,值为配置
- Field - 用于数组元素;字段将按
"string"、"int"等基本类型进行反序列化(用于数组包含基本类型的情况) - ArrayConfig - 用于数组元素;将数组反序列化为数组
示例:
{
"fields": {
"title": {
"base_field": {
"type": "string",
"path": "type"
}
}
}
}
ArrayConfig
数组和字段的配置
type ArrayConfig struct {
RootPath string `json:"root_path" yaml:"root_path"`
Reverse bool `yaml:"reverse" json:"reverse"`
ItemConfig *ObjectConfig `json:"item_config" yaml:"item_config"`
LengthLimit uint32 `json:"length_limit" yaml:"length_limit"`
Condition string `json:"condition" yaml:"condition"`
ItemCondition string `json:"item_condition" yaml:"item_condition"`
StaticConfig *StaticArrayConfig `json:"static_array" yaml:"static_array"`
}
- RootPath - 用于查找数组根元素的选择器;在 HTML 解析时也用于查找重复元素。数组大小为根元素下子元素的数量
- Reverse - bool[false] - 指示是否需要反向迭代(从 n 到 1)
- LengthLimit - 数组的固定长度(仅适用于生成的数组;不适用于静态数组)。注意:当源元素少于此限制时,数组会在末尾填充
null,以保持声明的大小(这是有意设计的)——省略length_limit可使数组长度与源长度完全一致 - Condition - 可选的 condition 表达式,在解析之前针对源节点进行求值;如果结果为 false,则从父对象中省略整个数组
- ItemCondition - 可选的 condition 表达式,对每个已构建的元素进行求值(fRes - 元素值,fSrc - 源元素,fIndex - 元素索引);解析结果为 false 的元素会从数组中移除——即声明式过滤。不适用于 static_array
配置可以是以下任意一种:
- ItemConfig - 数组每个元素的配置
- StaticConfig - 静态数组的配置
示例:
{
"root_path": "#content dt.quote > a",
"item_config": {
"field": {
"type": "string"
}
}
}
Field
字段通用配置
type Field struct {
BaseField *BaseField `json:"base_field" yaml:"base_field"`
ObjectConfig *ObjectConfig `json:"object_config" yaml:"object_config"`
ArrayConfig *ArrayConfig `json:"array_config" yaml:"array_config"`
FirstOf []*Field `json:"first_of" yaml:"first_of"`
}
配置可以是以下任意一种:
- BaseField - 字段将按
"string"、"int"等基本类型进行反序列化 - ObjectConfig - 字段为嵌套对象时使用
- ArrayConfig - 字段为数组时使用
- FirstOf - 选择第一个解析结果非空的字段
示例:
{
"base_field": {
"type": "string",
"path": "div.current-temp span.heading"
}
}
BaseField
用于获取静态信息或生成新信息
type BaseField struct {
Type FieldType `yaml:"type" json:"type"`
Path string `yaml:"path" json:"path"`
HTMLAttribute string `json:"html_attribute" yaml:"html_attribute"`
Condition string `json:"condition" yaml:"condition"`
Generated *GeneratedFieldConfig `yaml:"generated" json:"generated"`
FirstOf []*BaseField `json:"first_of" yaml:"first_of"`
}
- FieldType - enum["null", "boolean", "string", "int", "int64", "float", "float64", "array", "object", "html", "raw_string"] - 用于解析的静态字段。重要:html 类型仅适用于返回 HTML 的连接器(在此情况下,HTMLAttribute 不起作用)。示例
- Path - 用于解析的选择器(作为数组子元素时为相对路径)
- HTMLAttribute - 额外值,仅在通过 goquery 进行 HTML 解析时生效。可在此指定要解析的属性。
- Condition - 可选的 condition 表达式,对提取的值(fRes/fResJson/fResRaw、fIndex)进行求值;fSrc 为解析该字段时所依据的节点,包含同级节点。若结果为 false,则从父对象/数组中省略该字段,而不是生成 null。在 Generated 之前求值,因此条件为 false 时也会跳过生成操作(子请求、文件下载)
重要:默认情况下,"string" 类型会进行修剪并替换所有特殊字符;若需要纯字符串,请使用 "raw_string"
配置可以是以下任意一种,也可以为空:
示例
{
"generated": {
"uuid": {}
}
}
{
"type": "string",
"path": "text()"
}
条件字段
每个字段都可以带有 condition,即一个 expr-lang 表达式(预定义值)。如果表达式的计算结果不为 true,该字段就会从输出中省略(键/项会消失),而不是设为 null。无效表达式也会导致字段被省略,并记录错误。
条件的计算时机:
- BaseField.
condition- 提取之后:fRes是提取出的值,fSrc是解析该字段的节点(包括其同级节点),因此fSrc.on_sale == true可以根据未提取的数据来决定是否包含某个字段。条件为 false 时,会完全跳过生成工作(不发起子请求,也不下载文件) - ObjectConfig.
condition/ ArrayConfig.condition- 解析之前:fRes/fSrc是源节点(对于 json,是解析后的值;对于 html,是文本内容) - ArrayConfig.
item_condition- 针对每个已构建的项进行计算:fRes是该项,fSrc是构建该项所用的源元素,fIndex是该项的索引;条件为 false 的项会被丢弃,即声明式数组过滤。使用fSrc可根据源属性进行过滤,而无需将其添加到输出中
过滤数组项 - fSrc.in_stock 读取源元素(未提取到输出中),fRes.price 读取已构建的项:
{
"array_config": {
"root_path": "products",
"item_condition": "fSrc.in_stock && fRes.price > 0",
"item_config": {
"fields": {
"title": { "base_field": { "type": "string", "path": "title" } },
"price": { "base_field": { "type": "float", "path": "price" } }
}
}
}
}
仅当值通过检查时才包含键:
{
"discount": {
"base_field": {
"type": "float",
"path": "discount_pct",
"condition": "fRes > 0"
}
}
}
特殊情况:
- 在静态数组中,被省略的项仍会保留为
null(位置由定义固定,索引不会移动) - 如果省略了根模型配置,解析结果为
null - 在 first_of 中,条件为 false 的分支视为空,因此会尝试下一个分支
可运行示例:examples/config_conditions.json
GeneratedFieldConfig
提供在运行期间生成字段的功能
type GeneratedFieldConfig struct {
UUID *UUIDGeneratedFieldConfig `yaml:"uuid" json:"uuid"`
Static *StaticGeneratedFieldConfig `yaml:"static" json:"static"`
Formatted *FormattedFieldConfig `json:"formatted" yaml:"formatted"`
Plugin *PluginFieldConfig `yaml:"plugin" json:"plugin"`
Calculated *CalculatedConfig `yaml:"calculated" json:"calculated"`
File *FileFieldConfig `yaml:"file" json:"file"`
Model *ModelField `yaml:"model" json:"model"`
FileStorageField *FileStorageField `json:"file_storage" yaml:"file_storage"`
}
配置可以是以下类型之一:
- UUID - 生成随机 UUID V4
- Static - 生成静态字段
- Formatted - 格式化字段
- Model - 由其他连接器和模型生成的模型
- Plugin - 插件字段
- Calculated - 计算字段
- File - 文件字段(用于从服务下载文件)
- FileStorage - 可保存到本地文件的文件字段
示例:
{
"uuid": {}
}
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_cli.json#L58
{
"model": {
"type": "array",
"model": {
"array_config": {
"root_path": "#content dt.quote > a",
"item_config": {
"field": {
"type": "string"
}
}
}
},
"connector_config": {
"response_type": "HTML",
"url": "http://www.quotationspage.com/random.php",
"attempts": 3,
"browser_config": {
"chromium": {
"path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"wait": 10000
}
}
}
}
}
UUID
在运行期间生成随机 UUID V4,可用于生成唯一 ID
type UUIDGeneratedFieldConfig struct {
Regexp string `yaml:"regexp" json:"regexp"`
}
- Regexp - 提供一个匹配器,可用于获取生成的 UUID 的部分内容
Static
生成静态字段
type StaticGeneratedFieldConfig struct {
Type FieldType `yaml:"type" json:"type"`
Value string `json:"value" yaml:"value"`
Raw json.RawMessage `json:"raw" yaml:"raw"`
}
- Type - 枚举值["null", "boolean", "string", "int","int64","float","float64", "array", "object"] - 字段的类型
- Value - 字段的字符串值
- Raw - 字段的原始 json 值
示例
{
"type": "int",
"value": "65"
}
{
"type": "array",
"value": "[65,45]"
}
{
"type": "array",
"raw": [65,45]
}
Formatted Field Config
生成格式化字段,该字段会传递父级 base field 的值
type FormattedFieldConfig struct {
Template string `yaml:"template" json:"template"`
}
示例: https://github.com/PxyUp/fitter/blob/master/examples/cli/config_cli.json#L98
{
"template": "https://news.ycombinator.com/item?id={PL}"
}
文件存储字段
该字段可用于将字段结果存储为本地文件
type FileStorageField struct {
Content string `json:"content" yaml:"content"`
Raw json.RawMessage `yaml:"raw" yaml:"raw"`
FileName string `json:"file_name" yaml:"file_name"`
Path string `json:"path" yaml:"path"`
Append bool `json:"append" yaml:"append"`
}
- Content - 内容模板字符串。重要:可以将父级值作为字符串注入
- Raw - 字段的原始 JSON 内容。重要:可以将父级值作为字符串注入
- FileName - 用于存储文件的本地文件名。默认情况下,尝试先从标头获取 FileName,之后从 URL 获取。重要:可以将父级值作为字符串注入。
- Path - 用于存储文件的本地父目录。默认路径为进程目录。重要:可以将父级值作为字符串注入
- Append[false] - 是否追加到文件
{
"content": "{{{id}}}, {{{message}}}\n",
"append": true,
"file_name": "{{{id}}}.csv",
"path": "/Users/pxyup/fitter/examples/cli/test/csv"
}
文件字段
该字段可用于从服务器下载文件到本地
type FileFieldConfig struct {
Config *ServerConnectorConfig `yaml:"config" json:"config"`
Url string `yaml:"url" json:"url"`
FileName string `json:"file_name" yaml:"file_name"`
Path string `json:"path" yaml:"path"`
}
- Config - ServerConfig,使用默认的 fitter http.Client 发送请求
- Url - 图片的 URL。重要:连接器中的 URL 可以将父级值作为字符串注入
- FileName - 用于存储文件的本地文件名。默认情况下,尝试先从标头获取 FileName,之后从 URL 获取。重要:可以将父级值作为字符串注入。
- Path - 用于存储文件的本地父目录。默认路径为进程目录。重要:可以将父级值作为字符串注入
该字段的结果将是一个表示本地文件路径的字符串
{
"url": "https://images.shcdn.de/resized/w680/p/dekostoff-gobelinstoff-panel-oriental-cat-46-x-46_P19-KP_2.jpg",
"path": "/Users/pxyup/fitter/bin",
"config": {
"method": "GET"
}
}
包含传递的 URL(将父级值作为字符串注入)
{
"url": "https://picsum.photos{PL}",
"path": "/Users/pxyup/fitter/bin",
"config": {
"method": "GET"
}
}
配置示例:
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_image.json
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_image_multiple.json
计算字段
该字段可根据表达式生成不同类型的值
type CalculatedConfig struct {
Type FieldType `yaml:"type" json:"type"`
Expression string `yaml:"expression" json:"expression"`
}
- Type - 表达式的结果类型\
- Expression - 用于计算的表达式(我们使用这个库来计算表达式)
预定义值
FNull - builder.Nullvalue 的别名
FNil - nil 的别名
isNull(value T) - 用于检查 value 是否为 FNull 的函数
fRes - 来自解析的基础字段的原始(带有正确类型)结果
fIndex - 在父级数组中的索引(仅当父级是数组字段时)
fResJson - 原始结果的 JSON 字符串表示形式
fResRaw - 字节格式的结果
fSrc - 仅用于 condition/item_condition 表达式:值所解析自的源节点(对于 JSON,是解析后的值,包含同级项;对于 HTML,是文本内容)。在计算、格式化和通知器表达式中不可用
FNewLine - 换行符分隔符
{
"type": "bool",
"expression": "fRes > 500"
}
插件字段
该字段可以是 fitter 的外部插件
type PluginFieldConfig struct {
Name string `json:"name" yaml:"name"`
Config json.RawMessage `json:"config" yaml:"config"`
}
- Name - 插件名称(不带扩展名,仅名称)
- Config - 插件的 JSON 配置
模型字段
type ModelField struct {
// Type of parsing
ConnectorConfig *ConnectorConfig `yaml:"connector_config" json:"connector_config"`
// Model of the response
Model *Model `yaml:"model" json:"model"`
Type FieldType `yaml:"type" json:"type"`
Path string `yaml:"path" json:"path"`
Expression string `yaml:"expression" json:"expression"`
}
- ConnectorConfig - 要使用的连接器。重要:连接器中的 URL 可以将父级值作为字符串注入
- Model - 底层模型的配置
- Type - enum["null", "boolean", "string", "int", "int64", "float", "float64", "array", "object"] - 生成字段的类型
- Path - 如果无法从生成的字段中提取某些信息,可以使用 JSON 选择器进行提取
- Expression - 可用于对 Model 进行后处理的字符串(忽略 path 字段)
示例:
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_cli.json#L60
{
"type": "array",
"model": {
"array_config": {
"root_path": "#content dt.quote > a",
"item_config": {
"field": {
"type": "string"
}
}
}
}
}
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_weather.json#L37
{
"type": "string",
"path": "temp.temp",
"model": {
"object_config": {
"fields": {
"temp": {
"base_field": {
"type": "string",
"path": "//div[@id='forecast_list_ul']//td/b/a/@href",
"generated": {
"model": {
"type": "string",
"model": {
"object_config": {
"fields": {
"temp": {
"base_field": {
"type": "string",
"path": "div.current-temp span.heading"
}
}
}
}
},
"connector_config": {
"response_type": "HTML",
"attempts": 4,
"url": "https://openweathermap.org{PL}",
"browser_config": {
"playwright": {
"timeout": 30,
"wait": 30,
"install": false,
"browser": "FireFox",
"type_of_wait": "networkidle"
}
}
}
}
}
}
}
}
}
},
"connector_config": {
"response_type": "xpath",
"attempts": 3,
"url": "https://openweathermap.org/find?q={PL}",
"browser_config": {
"playwright": {
"timeout": 30,
"wait": 30,
"install": false,
"browser": "Chromium"
}
}
}
}
静态数组配置
提供静态(固定长度)数组生成
type StaticArrayConfig struct {
Items map[uint32]*Field `yaml:"items" json:"items"`
Length uint32 `yaml:"length" json:"length"`
}
示例:
{
"0": {
"base_field": {
"type": "string",
"path": "div.current-temp span.heading"
}
}
}
{
"length": 4,
"0": {
"base_field": {
"type": "string",
"path": "div.current-temp span.heading"
}
}
}
{
"length": 4,
"2": {
"base_field": {
"type": "string",
"path": "div.current-temp span.heading"
}
}
}
占位符列表
- {PL} - 用于注入值
- {INDEX} - 用于注入父数组中的索引
- {HUMAN_INDEX} - 用于以人类可读的方式注入父数组中的索引
- {{{json_path}}} - 从传播的“object”/“array”字段中获取信息
- {{{RefName=SomeName}}} - 按名称获取 引用 值。示例
- {{{RefName=SomeName json.path}}} - 按名称获取 引用 值,并通过 JSON 路径提取值。示例
- {{{FromEnv=ENV_KEY}}} - 从环境变量获取值
- {{{FromExp=fRes + 5 + fIndex}}} - 从表达式获取值。预定义值
- {{{FromInput=.}}} 或 {{{FromInput=json.path}}} - 从触发器或库的输入中获取值
- {{{FromFile=./test_file.log}}} - 按路径从文件中获取值。文件内容也可以包含占位符
- {{{FromURL=http://localhost:8081}}} - 从 URL 获取响应
示例:
{{{FromExp="{{{FromEnv=TEST_VAL}}}" + "hello"}}}
Current time is: {PL} with token from TokenRef={{{RefName=TokenRef}}} and TokenObjectRef={{{RefName=TokenObjectRef token}}}
Current time is: {PL} with token from TokenRef={{{RefName=TokenRef}}} and TokenObjectRef={{{RefName=TokenObjectRef token}}}
TokenRef={{{RefName=TokenRef}}} and TokenObjectRef={{{RefName=TokenObjectRef token}}} Object={{{value}}} {PL} Env={{{FromEnv=TEST_VAL}}} {INDEX} {HUMAN_INDEX}
引用
一种特殊映射,会在任何处理开始前预先获取,可用于连接器或占位符
可用于:
- 缓存 JWT 令牌并在标头中使用
- 缓存值
- 等等
引用
type Reference struct {
*ModelField
Expire *uint32 `yaml:"expire" json:"expire"`
}
- ModelField - 嵌入式结构体,可使用相同的字段
- Expire[sec] - 获取后引用过期的时长。未设置 => 永久缓存。设为 0 => 每次都重新获取。设为 n > 0 => 缓存 n 秒
用于 Fitter
type RefMap map[string]*Reference
type Config struct {
// Other Config Fields
Limits *Limits `yaml:"limits" json:"limits"`
References RefMap `json:"references" yaml:"references"`
}
用于 Fitter Cli
type RefMap map[string]*Reference
type CliItem struct {
// Other Config Fields
Limits *Limits `yaml:"limits" json:"limits"`
References RefMap `json:"references" yaml:"references"`
}
示例
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_ref.json#L2
{
"references": {
"TokenRef": {
"expire": 10,
"connector_config": {
"response_type": "json",
"static_config": {
"value": "\"plain token\""
}
},
"model": {
"base_field": {
"type": "string"
}
}
},
"TokenObjectRef": {
"connector_config": {
"response_type": "json",
"static_config": {
"value": "{\"token\":\"token from object\"}"
}
},
"model": {
"object_config": {
"fields": {
"token": {
"base_field": {
"type": "string",
"path": "token"
}
}
}
}
}
}
}
}
通知器
可选的单项配置 item.notifier_config,用于在处理后将解析结果推送到其他位置。结果仍会像往常一样返回(CLI/MCP 输出、服务日志);通知器会额外发送结果。适用于 Fitter(服务模式)、Fitter_CLI 和 Fitter_MCP。
type NotifierConfig struct {
Expression string `yaml:"expression" json:"expression"`
Force bool `json:"force" yaml:"force"`
SendArrayByItem bool `yaml:"send_array_by_item" json:"send_array_by_item"`
Template string `yaml:"template" json:"template"`
// exactly ONE destination:
Console *ConsoleConfig `yaml:"console" json:"console"`
TelegramBot *TelegramBotConfig `yaml:"telegram_bot" json:"telegram_bot"`
Http *HttpConfig `yaml:"http" json:"http"`
Redis *RedisNotifierConfig `json:"redis" yaml:"redis"`
File *FileStorageField `json:"file" yaml:"file"`
}
- Expression - 可选的 expr-lang 条件:仅当条件求值为 true 时才发送通知。解析结果可通过
fRes(解析后的值)、fResRaw(原始字节)、fResJson(JSON 字符串)获取,例如len(fResRaw) > 0 - Force - 即使解析以错误结束,也发送通知
- SendArrayByItem - 如果结果是数组,则将每个元素作为单独的通知发送
- Template - 可选模板,在发送前应用于结果,允许使用占位符
- Destination - 只能是
console、telegram_bot、http、redis、file中的一个
目标配置:
type HttpConfig struct {
Url string `yaml:"url" json:"url"`
Method string `json:"method" yaml:"method"`
Headers map[string]string `yaml:"headers" json:"headers"`
Timeout uint32 `yaml:"timeout" json:"timeout"`
}
type TelegramBotConfig struct {
Token string `json:"token" yaml:"token"`
UsersId []int64 `json:"users_id" yaml:"users_id"`
Pretty bool `json:"pretty" yaml:"pretty"`
OnlyMsg bool `json:"only_msg" yaml:"only_msg"`
}
type RedisNotifierConfig struct {
Addr string `json:"addr" yaml:"addr"`
Password string `json:"password" yaml:"password"`
DB int `json:"db" yaml:"db"`
Channel string `json:"channel" yaml:"channel"`
}
type ConsoleConfig struct {
OnlyResult bool `json:"only_result" yaml:"only_result"`
}
file 目标使用与 file 字段类型相同的 FileStorageField。
示例(examples/config_telegram.json):
{
"item": {
"connector_config": { "...": "..." },
"model": { "...": "..." },
"notifier_config": {
"expression": "len(fResRaw) > 0",
"telegram_bot": {
"token": "{{{FromEnv=TG_TOKEN}}}",
"users_id": [123456],
"pretty": true
}
}
}
}
限制
设置限制,以防止 DDOS 和内存占用过高
type Limits struct {
HostRequestLimiter HostRequestLimiter `yaml:"host_request_limiter" json:"host_request_limiter"`
ChromiumInstance uint32 `yaml:"chromium_instance" json:"chromium_instance"`
DockerContainers uint32 `yaml:"docker_containers" json:"docker_containers"`
PlaywrightInstance uint32 `yaml:"playwright_instance" json:"playwright_instance"`
}
- HostRequestLimiter - map[string]int64 - 按主机名设置限制,键为主机,值为并行请求数(用于服务连接器)
- ChromiumInstance - 并行 chromium 实例数
- DockerContainers - 并行 docker 实例数
- PlaywrightInstance - 并行 playwright 实例数
https://github.com/PxyUp/fitter/blob/master/examples/cli/config_cli.json#L2
{
"limits": {
"host_request_limiter": {
"hacker-news.firebaseio.com": 5
},
"chromium_instance": 3,
"docker_containers": 3,
"playwright_instance": 3
}
}
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。