Bookshelf API v2 参考文档解析:wigolo 提取基准中 API 参考类 golden 样本的规范化 Markdown 形态
【免费下载链接】wigoloThe go-to web for your AI coding agent — local-first search, fetch, crawl & research over MCP. No API keys, no cloud, $0/query. Public beta.项目地址: https://gitcode.com/GitHub_Trending/wi/wigolo
本篇指南以 api-001.md(一份完整规范的 REST API 参考文档样例)为核心样本,深入讲解它在 wigolo 项目内容提取基准测试体系中的角色:它是一份golden 期望输出,用于量化评估 HTML→Markdown 提取器在"API 参考文档"这一页面类别上的质量。读完本文,你将理解 golden 文件的格式约定、提取基准的评测指标与底层管线实现,并能在仓库中新增或维护同类评估样本。
golden 文件的定位:它服务于什么
benchmarks/extraction/fixtures/golden/api-001.md不是一份普通的文档,而是 wigolo 提取(extraction)基准测试的金标样本。基准测试的整体思路是:给定一份真实网页的 HTML 夹具,让内容提取器把它转换成 Markdown,再将转换结果与人工整理好的"期望输出"(即 golden 文件)逐项比对,用数值化指标衡量提取质量。
在 manifest.json 中,api-001条目定义了该样本的元信息:
{ "id": "api-001", "url": "https://api.example.com/docs/v2", "category": "docs", "htmlFixturePath": "html/api-001.html", "goldenPath": "golden/api-001.md", "expectedExtractor": "defuddle", "tags": ["api", "rest", "endpoints"] }从字段可以看出:该样本对应一个 API 文档页面(category: "docs",tags标注为 API/REST/端点),其 HTML 夹具位于benchmarks/extraction/fixtures/html/(由htmlFixturePath指向),而goldenPath指向本文件——这份api-001.md。expectedExtractor为defuddle,意味着基准运行时希望主提取器判定该页面应由 defuddle 提取器负责,这一字段在 runner.ts 中会与实际使用的提取器比对,得到extractorMatch指标。
因此,golden 文件本质上是"人工标定的正确答案",它同时约束了两件事:内容要提取到什么程度(哪些段落、表格、代码块必须保留),以及格式应呈现为什么样子(标题层级、表格、代码围栏等 Markdown 语法特征)。
样本内容全解:Bookshelf API v2 参考文档
golden 样本的正文是一份虚构的图书管理服务 "Bookshelf API" 的 v2 接口参考文档。它覆盖了生产级 REST API 文档的典型组成要素:认证、限流、统一响应结构、资源端点、错误码表、分页、Webhook 与变更日志。以下逐节展开。
Base URL 与版本前缀
https://api.bookshelf.example.com/v2所有端点均基于该基础地址,v2路径前缀用于版本隔离。文档明确约定所有端点返回 JSON,且需要通过 Bearer token 认证——这是多数现代 REST API 的通用基线约定。
认证方式
在请求头中携带 API 密钥:
Authorization: Bearer sk_live_abc123def456未携带有效 token 的请求统一收到401 Unauthorized响应。注意这里用的是sk_live_前缀的密钥,属于服务端密钥(live key)命名惯例,与测试密钥sk_test_区分。
限流策略
API 按密钥维度实施速率限制,不同套餐的配额不同:
| Plan | Requests/min | Burst |
|---|---|---|
| Free | 60 | 10 |
| Pro | 600 | 50 |
| Enterprise | 6000 | 200 |
Burst(突发配额)表示在短时间内允许超出每分钟配额的请求数,用于容忍短时尖峰。每次响应都会携带限流头,客户端可以据此感知剩余额度:
X-RateLimit-Limit: 600 X-RateLimit-Remaining: 594 X-RateLimit-Reset: 1713187200X-RateLimit-Reset是 Unix 时间戳,表示配额重置时刻;配合X-RateLimit-Remaining,客户端可实现优雅退避(backoff)策略,避免触发429 rate_limited。
统一响应封装(envelope)
所有成功响应遵循统一信封结构,data承载业务数据,meta携带请求追踪信息:
{ "data": {}, "meta": { "request_id": "req_7f3a9b2c", "timestamp": "2026-04-15T10:30:00Z" } }错误响应使用同一信封,但用error字段替换data,并包含机器可读的code、人类可读的message以及 HTTPstatus:
{ "error": { "code": "not_found", "message": "Book with ID 999 does not exist.", "status": 404 }, "meta": { "request_id": "req_8e4b0c3d", "timestamp": "2026-04-15T10:30:01Z" } }统一的request_id让调用方可以把一次失败请求与后端日志、追踪系统关联起来,是生产 API 的重要可观测性设计。
图书(Books)端点
列出图书:GET /books
返回分页图书列表,支持以下查询参数:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Items per page (max 100) |
sort | string | title | Sort field:title,year,rating |
order | string | asc | Sort order:ascordesc |
genre | string | — | Filter by genre slug |
q | string | — | Full-text search across title/author |
一个按流派过滤并限制条数的实际请求:
curl -H "Authorization: Bearer sk_live_abc123def456" \ "https://api.bookshelf.example.com/v2/books?genre=sci-fi&limit=2"响应中的meta.pagination携带分页元数据(page、limit、total、total_pages),业务数组位于data:
{ "data": [ { "id": 42, "title": "Neuromancer", "author": { "id": 7, "name": "William Gibson" }, "year": 1984, "genre": "sci-fi", "isbn": "978-0-441-56956-4", "rating": 4.2, "pages": 271 }, { "id": 108, "title": "Snow Crash", "author": { "id": 15, "name": "Neal Stephenson" }, "year": 1992, "genre": "sci-fi", "isbn": "978-0-553-38095-8", "rating": 4.0, "pages": 468 } ], "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-15T10:32:00Z", "pagination": { "page": 1, "limit": 2, "total": 87, "total_pages": 44 } } }获取单本图书:GET /books/:id
按数字 ID 返回图书详情,字段比列表更完整,包含description、tags、created_at、updated_at:
{ "data": { "id": 42, "title": "Neuromancer", "author": { "id": 7, "name": "William Gibson", "bio": "American-Canadian speculative fiction writer." }, "year": 1984, "genre": "sci-fi", "isbn": "978-0-441-56956-4", "rating": 4.2, "pages": 271, "description": "The sky above the port was the color of television, tuned to a dead channel.", "tags": ["cyberpunk", "ai", "hacking"], "created_at": "2025-01-10T08:00:00Z", "updated_at": "2026-03-22T14:15:00Z" } }创建图书:POST /books
需要write权限 scope。请求体通过author_id关联作者:
{ "title": "The Dispossessed", "author_id": 22, "year": 1974, "genre": "sci-fi", "isbn": "978-0-06-051275-2", "pages": 387, "description": "A brilliant physicist decides to take action.", "tags": ["utopia", "anarchism", "physics"] }成功返回201 Created及完整对象(此时rating为null,待后续评分):
{ "data": { "id": 253, "title": "The Dispossessed", "author": { "id": 22, "name": "Ursula K. Le Guin" }, "year": 1974, "genre": "sci-fi", "isbn": "978-0-06-051275-2", "rating": null, "pages": 387, "created_at": "2026-04-15T10:35:00Z", "updated_at": "2026-04-15T10:35:00Z" } }更新图书:PATCH /books/:id
部分更新语义——请求体只包含要修改的字段,例如更新评分与标签:
{ "rating": 4.5, "tags": ["utopia", "anarchism", "physics", "award-winner"] }成功返回200 OK与更新后的完整图书对象。PATCH与PUT的差异正在于此:前者支持字段级局部更新,后者要求提交完整资源。
删除图书:DELETE /books/:id
永久删除图书记录,成功返回204 No Content(无响应体)。
作者(Authors)端点
GET /authors支持page、limit(默认 20,最大 100)与按姓名搜索的q参数;GET /authors/:id返回作者信息及其图书摘要:
{ "data": { "id": 7, "name": "William Gibson", "bio": "American-Canadian speculative fiction writer and essayist.", "born": 1948, "website": "https://williamgibsonbooks.com", "book_count": 12, "books": [ { "id": 42, "title": "Neuromancer", "year": 1984 }, { "id": 43, "title": "Count Zero", "year": 1986 }, { "id": 44, "title": "Mona Lisa Overdrive", "year": 1988 } ] } }注意books数组中的条目是轻量摘要(只含id/title/year),与图书详情端点的完整字段形成对照——这是 API 文档中常见的"列表瘦身"模式。
阅读清单(Reading Lists)端点
创建清单:
POST /lists{ "name": "Summer 2026", "description": "Books to read this summer", "visibility": "public", "book_ids": [42, 108, 253] }向清单添加图书:POST /lists/:list_id/books,请求体为{"book_id": 77},返回200 OK与更新后的清单。从清单移除图书:DELETE /lists/:list_id/books/:book_id,返回204 No Content。visibility字段暗示清单具备公开/私有两种可见性,属于权限模型的典型设计。
错误码表
| Code | Status | Description |
|---|---|---|
bad_request | 400 | Malformed request body or params |
unauthorized | 401 | Missing or invalid API key |
forbidden | 403 | Insufficient scope for this action |
not_found | 404 | Resource does not exist |
conflict | 409 | Duplicate ISBN or resource conflict |
rate_limited | 429 | Too many requests |
internal_error | 500 | Unexpected server error |
unauthorized与forbidden的区分(认证失败 vs 权限不足)呼应了前文"需要writescope"的授权模型;conflict专门覆盖 ISBN 重复这类唯一性冲突。
分页:游标模式
除页码外,所有列表端点还支持游标式分页:
GET /books?cursor=eyJpZCI6NDJ9&limit=20游标是一个不透明字符串(示例中为 base64 编码的记录定位信息),比页码更稳定——避免新增/删除数据导致页码漂移。当还有更多结果时,meta.pagination会返回has_more与next_cursor:
{ "meta": { "pagination": { "limit": 20, "has_more": true, "next_cursor": "eyJpZCI6NjJ9" } } }Webhook 订阅
客户端可注册 Webhook 以接收资源变更事件:
POST /webhooks{ "url": "https://yourapp.example.com/hooks/bookshelf", "events": ["book.created", "book.updated", "book.deleted"], "secret": "whsec_your_signing_secret" }每次投递都会携带X-Signature头用于验签(配合secret做 HMAC 校验),这是防止伪造回调的标准做法。事件名采用<resource>.<action>的命名约定,便于按资源前缀订阅。
变更日志(Changelog)
- v2.3(2026-04-01) — 新增游标分页
- v2.2(2026-02-15) — 新增 Webhook 支持
- v2.1(2025-11-01) — 新增阅读清单端点
- v2.0(2025-08-01) — v2 首发,引入新认证模型
延伸阅读区块
样本文档以"Further Reading"结尾,列出认证指南、Webhook 验签、SDK 参考与状态页等后续阅读入口(样例中为指向文档站内部的示例链接)。这一区块与正文一样被保留在 golden 中——它说明完整提取不应丢弃文档页尾的导航性内容。
提取基准如何用这份 golden 评估质量
golden 的价值最终在基准运行时兑现。runner.ts 是提取基准的执行入口(通过npm run bench:extraction调用,见 package.json),其核心流程为:
- 加载清单:
loadManifest读取 manifest.json,并可用--filter按category、id或tags筛选样本(filterManifestEntries); - 装载夹具:
loadFixtureHtml读入html/api-001.html,loadGoldenMarkdown读入本 golden 文件; - 执行提取:
runSingleBenchmark调用extractContent(html, url)(入口为 pipeline.ts),记录耗时与所选提取器,并校验extractor === expectedExtractor; - 计算指标:
computeMetrics对比提取结果与 golden,产出 Precision、Recall、F1、ROUGE-L 以及标题数/链接数是否一致(metrics.ts); - 汇总报告:
computeSummary与generateMarkdownReport按整体、按类别(category)、按提取器(extractor)三层输出统计,并写入extraction-benchmark.json与extraction-benchmark.md(report.ts)。
指标的具体含义
以api-001为例,评测时提取结果与 golden 都会被送入 tokenizer.ts 做归一化:去掉 Markdown 标记(标题#、列表符号、围栏代码块、加粗/行内代码、链接目标),把空白折叠为单空格并转小写,再按非字母数字边界切成 token 集合:
- Precision:提取结果中属于 golden 的 token 比例(有没有提取"多余"的东西);
- Recall:golden 中被提取结果覆盖的 token 比例(有没有"漏掉"正文内容);
- F1:两者的调和平均,是衡量提取保真度的综合指标;
- ROUGE-L:基于最长公共子序列(LCS)的相似度,对 token 顺序敏感,能捕捉"内容齐全但顺序混乱"的质量问题;
- Heading / Link 匹配:直接比对标题数量与链接数量是否与 golden 一致(metrics.ts),用于捕获"整段丢失"或"提取了无关导航链接"等结构性问题。
对于像api-001这样表格与代码块密集的 API 文档,countHeadings与countLinks尤其重要:页面导航、页脚链接若被错误保留,会直接拉低 Link 匹配率。
底层提取管线如何还原这类 Markdown 形态
golden 文件的格式特征——ATX 风格标题(#)、围栏代码块(```)、GFM 表格(| --- |分隔行)、行内代码——与提取管线的 HTML→Markdown 转换器输出是严格对齐的。
转换核心位于 markdown.ts:buildTurndown基于 TurndownService 构建,显式配置了headingStyle: 'atx'(标题用#而非 setext 下划线)与codeBlockStyle: 'fenced'(代码块用围栏而非缩进)。其中两个自定义规则直接解释了 golden 中表格和代码块的呈现:
- table 规则:
<table>被整体转换为 GFM 表格,首行作为表头并生成| --- |分隔行,单元格文本中的换行被折叠为空格(markdown.ts)。这也正是 golden 中大量参数表、错误码表、限流表能整齐呈现的原因; - codeBlockLang 规则:
<pre><code>的 class 会经过detectCodeLanguage(来自 lang-hints.ts)推断语言名,围栏长度会根据代码内容中最长的反引号连续段自动加长,避免嵌套反引号破坏代码块(markdown.ts)。
在管线层面,pipeline.ts 的applyPostProcessing会对原始转换结果做一系列后处理:resolveRelativeUrls把相对链接/图片解析为基于页面 URL 的绝对地址;stripBoilerplateMarkdown剔除导航、页脚等样板内容;filterDecorativeImages按 URL 特征与 alt 文本过滤装饰性图片;sanitizeExtractedMarkdown做最终清洗。这些步骤共同决定了"最终进入指标比对的内容边界",而 golden 正是对这一边界的人工标定——例如 Bookshelf 文档中的Authorization头、curl 命令、JSON 响应体都属于正文必须保留,而装饰图标、埋点像素则不应出现。
如何新增与维护同类 golden 样本
如果你想为项目补充一个 API 参考类页面的评估样本,流程如下:
- 在
benchmarks/extraction/fixtures/html/下放置原始网页 HTML 夹具; - 在
benchmarks/extraction/fixtures/golden/下编写对应的期望 Markdown,格式约定与本文所示一致:ATX 标题、围栏代码块、GFM 表格、行内代码,只保留页面正文内容; - 在 manifest.json 的
entries数组中新增条目,填写id(唯一)、url、category(docs/article/github 等)、htmlFixturePath、goldenPath、expectedExtractor与tags; - 运行
npm run bench:extraction全量评测,或通过 filter 只跑新增样本,观察报告中的 F1、ROUGE-L 与 Heading/Link 匹配率。
编写 golden 时需注意:它应反映"理想提取结果"而非"当前实现的结果",否则指标会失去监督意义;同时应与 HTML 夹具保持一一对应,任何一方的改动都要同步评审,避免黄金标准失真。
【免费下载链接】wigoloThe go-to web for your AI coding agent — local-first search, fetch, crawl & research over MCP. No API keys, no cloud, $0/query. Public beta.项目地址: https://gitcode.com/GitHub_Trending/wi/wigolo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考