{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "BookSource",
"description": "v2 书源。",
"type": "object",
"required": [
"bookInfo",
"content",
"name",
"schema",
"toc",
"url"
],
"properties": {
"bookInfo": {
"$ref": "#/definitions/BookInfoOp"
},
"concurrentRate": {
"description": "限速:`\"N/ms\"`(N 次/ms)或纯毫秒间隔字符串;为空则用 `http.rateLimit`。",
"type": "string"
},
"content": {
"$ref": "#/definitions/ContentRules"
},
"enabledCookieJar": {
"description": "开启后:响应的 `Set-Cookie` 自动回灌进 cookie 库(按注册域归并持久化)。",
"default": false,
"type": "boolean"
},
"explore": {
"anyOf": [
{
"$ref": "#/definitions/ExploreOp"
},
{
"type": "null"
}
]
},
"fontMaps": {
"description": "命名共享 fontMap 表(`名字 -> {码点:真字}`):供多个 clean 步骤按名复用,避免同一张 大表在书源里重复内联。clean 的 `fontMap` 写字符串名(如 `{\"fontMap\":\"search\"}`)即引用本表, 在 [`BookSource::from_json`] 解析时就地展开为内联表(运行期与直接内联等价)。",
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"group": {
"type": "string"
},
"http": {
"default": {
"charset": "auto",
"cookies": {},
"fetcher": "auto",
"headers": {},
"warmup": []
},
"allOf": [
{
"$ref": "#/definitions/Http"
}
]
},
"loginCheckJs": {
"description": "登录态过期校验脚本:每个网络方法响应后执行(注入 `result`=响应),判失效可提示重登。",
"type": "string"
},
"loginUi": {
"description": "声明式登录表单(TUI 渲染);收集值加密存为 loginInfo,供 `login()` 脚本读取。",
"type": "array",
"items": {
"$ref": "#/definitions/RowUi"
}
},
"loginUrl": {
"description": "登录:普通 URL,或登录脚本(`@js:…` / `<js>…</js>` 包裹,内含 `login()` 函数)。 非空即视为「需要登录」(见 [`BookSource::has_login`]);仅 `js-host` 构建可真正执行脚本登录。",
"type": "string"
},
"name": {
"type": "string"
},
"samples": {
"type": "array",
"items": {
"$ref": "#/definitions/Sample"
}
},
"schema": {
"description": "固定为 `\"trnovel-booksource/v2\"`。",
"type": "string"
},
"search": {
"anyOf": [
{
"$ref": "#/definitions/ListPageSpec"
},
{
"type": "null"
}
]
},
"toc": {
"$ref": "#/definitions/TocRules"
},
"url": {
"description": "站点基址,用于相对链接解析与 `{{base}}`。",
"type": "string"
}
},
"additionalProperties": false,
"definitions": {
"BookInfoOp": {
"description": "书详情操作:详情字段抽取(同 [`BookRules`])+ 可选前置请求链(见 design D7-bis)。 字段与 `BookRules` 同名同序,故现有 `bookInfo:{...}` JSON 逐字节解析等价;引擎经 [`BookInfoOp::as_book_rules`] 复用既有 `eval_book_info`(不用 `flatten` 以保 `deny_unknown_fields`)。",
"type": "object",
"properties": {
"author": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"bookUrl": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"cover": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"intro": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"kind": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"lastChapter": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"name": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"prelude": {
"description": "详情主请求前的前置请求链;空 = 现状(直接 fetch book_url)。",
"type": "array",
"items": {
"$ref": "#/definitions/PreStep"
}
},
"tocUrl": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"wordCount": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
"BookRules": {
"description": "一本书的字段抽取规则(均可省略)。",
"type": "object",
"properties": {
"author": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"bookUrl": {
"description": "列表项:指向书详情页的链接(搜索/浏览结果用;bookInfo 阶段忽略)。",
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"cover": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"intro": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"kind": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"lastChapter": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"name": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"tocUrl": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"wordCount": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
"ByteEnc": {
"description": "crypto 的 key/iv/输入/输出字节编码。",
"oneOf": [
{
"type": "string",
"enum": [
"utf8",
"base64",
"hex"
]
},
{
"description": "原样字节(等同 utf8 字节,主要用于输入密文已是裸字节串的场景)。",
"type": "string",
"enum": [
"raw"
]
}
]
},
"Capture": {
"description": "一条结构化命名捕获:对**所属请求的响应**用 `value` 规则求一个字符串, 写入 `scope` 指定的作用域层,后续步骤/抽取规则以 `{{name}}` 引用。",
"type": "object",
"required": [
"name",
"value"
],
"properties": {
"name": {
"description": "变量名(后续以 `{{name}}` 引用,走现有模板插值)。",
"type": "string"
},
"scope": {
"description": "写入哪一层作用域;默认 `chapter`(本次调用临时)。",
"default": "chapter",
"allOf": [
{
"$ref": "#/definitions/VarScope"
}
]
},
"value": {
"description": "对响应求值的规则(复用 `Rule` AST,产物是字符串)。",
"allOf": [
{
"$ref": "#/definitions/Rule"
}
]
}
},
"additionalProperties": false
},
"Charset": {
"description": "字符集。",
"type": "string",
"enum": [
"auto",
"utf8",
"gbk",
"gb18030",
"big5"
]
},
"CipherAlgo": {
"description": "对称加密算法。",
"type": "string",
"enum": [
"aes",
"des",
"tripleDes"
]
},
"CipherMode": {
"description": "加密模式。",
"type": "string",
"enum": [
"cbc",
"ecb",
"cfb",
"gcm"
]
},
"CipherOp": {
"description": "加解密方向。",
"type": "string",
"enum": [
"decrypt",
"encrypt"
]
},
"CipherStep": {
"description": "加解密算子。默认值贴合「解密正文」主场景:`op=decrypt`、`inputEnc=base64`、`outputEnc=utf8`。",
"type": "object",
"required": [
"algo",
"key",
"mode"
],
"properties": {
"algo": {
"$ref": "#/definitions/CipherAlgo"
},
"inputEnc": {
"description": "入参密文串→字节;省略时按 `op` 取默认(decrypt→base64,encrypt→utf8)。",
"anyOf": [
{
"$ref": "#/definitions/ByteEnc"
},
{
"type": "null"
}
]
},
"iv": {
"type": [
"string",
"null"
]
},
"ivEnc": {
"default": "utf8",
"allOf": [
{
"$ref": "#/definitions/ByteEnc"
}
]
},
"key": {
"type": "string"
},
"keyEnc": {
"default": "utf8",
"allOf": [
{
"$ref": "#/definitions/ByteEnc"
}
]
},
"mode": {
"$ref": "#/definitions/CipherMode"
},
"op": {
"default": "decrypt",
"allOf": [
{
"$ref": "#/definitions/CipherOp"
}
]
},
"outputEnc": {
"description": "结果字节→串;省略时按 `op` 取默认(decrypt→utf8,encrypt→base64)。",
"anyOf": [
{
"$ref": "#/definitions/ByteEnc"
},
{
"type": "null"
}
]
},
"padding": {
"default": "pkcs7",
"allOf": [
{
"$ref": "#/definitions/Padding"
}
]
}
},
"additionalProperties": false
},
"CleanStep": {
"description": "单步后处理。步内多算子按固定顺序执行: `regex/replace → trim → prepend → append → decode → encode → hash → cipher → fontMap → cn`。",
"type": "object",
"properties": {
"append": {
"type": [
"string",
"null"
]
},
"cipher": {
"description": "对称加解密。",
"anyOf": [
{
"$ref": "#/definitions/CipherStep"
},
{
"type": "null"
}
]
},
"cn": {
"description": "繁简转换。",
"anyOf": [
{
"$ref": "#/definitions/CnConvert"
},
{
"type": "null"
}
]
},
"decode": {
"description": "解码(base64/base64url/hex/url)。",
"anyOf": [
{
"$ref": "#/definitions/Codec"
},
{
"type": "null"
}
]
},
"encode": {
"description": "编码(base64/base64url/hex/url)。",
"anyOf": [
{
"$ref": "#/definitions/Codec"
},
{
"type": "null"
}
]
},
"fontMap": {
"description": "字体反爬还原:私有区(PUA)字符按映射表换回真字。键为码点十六进制(如 `\"E4DE\"` 或 `\"U+E4DE\"`), 值为目标字符;表外字符原样保留。用于番茄等「自定义字体 + PUA」反爬站点——表是数据,由书源内联 提供(引擎不内置任何站点的表),可用 `trn gen-fontmap` 生成。",
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "string"
}
},
"hash": {
"description": "哈希/HMAC。",
"anyOf": [
{
"$ref": "#/definitions/HashStep"
},
{
"type": "null"
}
]
},
"js": {
"description": "JS 后处理(逃生舱;脚本里以当前串为 `result`)。需启用 `js` feature。",
"type": [
"string",
"null"
]
},
"prepend": {
"type": [
"string",
"null"
]
},
"regex": {
"type": [
"string",
"null"
]
},
"replace": {
"type": [
"string",
"null"
]
},
"trim": {
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
},
"CnConvert": {
"description": "繁简转换方向。",
"oneOf": [
{
"description": "繁体 → 简体。",
"type": "string",
"enum": [
"t2s"
]
},
{
"description": "简体 → 繁体。",
"type": "string",
"enum": [
"s2t"
]
}
]
},
"Codec": {
"description": "编解码方式(`decode`/`encode` 算子,以及 crypto 的字节↔串编码)。",
"oneOf": [
{
"type": "string",
"enum": [
"base64",
"base64url",
"hex"
]
},
{
"description": "URL 百分号编解码。",
"type": "string",
"enum": [
"url"
]
}
]
},
"ContentRules": {
"description": "正文规则(可选分页)。",
"type": "object",
"required": [
"value"
],
"properties": {
"maxPages": {
"default": 100,
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"nextPage": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"prelude": {
"description": "正文主请求之前按序执行的前置请求链(见 design D7-bis);空 = 现状。",
"type": "array",
"items": {
"$ref": "#/definitions/PreStep"
}
},
"value": {
"$ref": "#/definitions/Rule"
}
},
"additionalProperties": false
},
"EntrySource": {
"description": "入口源:静态固定入口 或 远端抓取入口。`explore.entries` 是其数组,按声明顺序合并 (无独立 chain 类型——「按序合并」即数组遍历)。与 [`Rule`] 一致用 untagged + 唯一键判别 (`static` / `fetch`)。",
"anyOf": [
{
"description": "静态入口列表。",
"type": "object",
"required": [
"static"
],
"properties": {
"static": {
"type": "array",
"items": {
"$ref": "#/definitions/StaticEntry"
}
}
}
},
{
"description": "远端抓取动态入口(`Box`:`FetchEntrySource` 含完整 `Request`,避免枚举变体尺寸悬殊; `Box<T>` 对 serde/schemars 透明,JSON 与 schema 形状不变)。",
"type": "object",
"required": [
"fetch"
],
"properties": {
"fetch": {
"$ref": "#/definitions/FetchEntrySource"
}
}
}
]
},
"Expect": {
"description": "样例期望不变量。",
"type": "object",
"properties": {
"minChapters": {
"type": [
"integer",
"null"
],
"format": "uint",
"minimum": 0.0
},
"minContentChars": {
"type": [
"integer",
"null"
],
"format": "uint",
"minimum": 0.0
},
"name": {
"type": [
"string",
"null"
]
},
"volumes": {
"type": [
"integer",
"null"
],
"format": "uint",
"minimum": 0.0
}
}
},
"ExploreOp": {
"description": "浏览操作:两阶段——`entries` 生成可选择的入口,`page` 用选中入口的变量取书。",
"type": "object",
"required": [
"entries",
"page"
],
"properties": {
"entries": {
"description": "入口源数组(按声明顺序合并;static + fetch 组合)。",
"type": "array",
"items": {
"$ref": "#/definitions/EntrySource"
}
},
"page": {
"description": "共享列表页规格:用选中入口变量 + page/pageSize 取书。",
"allOf": [
{
"$ref": "#/definitions/ListPageSpec"
}
]
}
},
"additionalProperties": false
},
"Extract": {
"description": "取值方式(枚举字符串 或 `{ \"attr\": \"...\" }`)。",
"anyOf": [
{
"$ref": "#/definitions/ExtractOp"
},
{
"type": "object",
"required": [
"attr"
],
"properties": {
"attr": {
"type": "string"
}
}
}
]
},
"ExtractOp": {
"description": "文本/HTML 取值算子。",
"type": "string",
"enum": [
"text",
"ownText",
"html",
"innerHtml",
"outerHtml"
]
},
"FetchEntryItem": {
"description": "fetch 入口项的生成规则:标题 + 变量。两者对「当前数据项(ctx)+ 外层 forEach 变量(vars)」求值, 故 `title`/`vars` 规则既能 `via:json` 读数据项字段,也能 `{{name}}` 引用循环变量。",
"type": "object",
"required": [
"title"
],
"properties": {
"title": {
"$ref": "#/definitions/Rule"
},
"vars": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Rule"
}
}
},
"additionalProperties": false
},
"FetchEntrySource": {
"description": "远端抓取入口源:请求远端分类数据 → `list` 抽数组 → 每项经 `item` 生成入口。 可选 `forEach` 按多组变量重复请求并合并入口(空 = 执行一次)。",
"type": "object",
"required": [
"item",
"list",
"request"
],
"properties": {
"forEach": {
"description": "循环变量集:对每组变量各请求一次、合并结果;变量在请求模板与 `item` 规则中可见。空 = 一次。",
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"item": {
"$ref": "#/definitions/FetchEntryItem"
},
"list": {
"description": "从响应抽取数据项数组(每项作为 `item` 规则的上下文)。",
"allOf": [
{
"$ref": "#/definitions/Rule"
}
]
},
"request": {
"description": "入口数据请求(复用 [`Request`]:支持 render / interceptApi / charset / headers / 模板)。",
"allOf": [
{
"$ref": "#/definitions/Request"
}
]
}
},
"additionalProperties": false
},
"FetchMode": {
"description": "取页模式:是否动用浏览器解反爬挑战。 真正是否开浏览器还需 app/用户级授权(两级取交集,见 OpenSpec change `browser-fetcher` D12)。",
"oneOf": [
{
"description": "默认:平时 reqwest,撞挑战才升级浏览器。",
"type": "string",
"enum": [
"auto"
]
},
{
"description": "永不开浏览器,撞挑战即降级。",
"type": "string",
"enum": [
"reqwest"
]
},
{
"description": "整站强制走浏览器(首请求即被挑战 / 整页 JS 渲染)。",
"type": "string",
"enum": [
"browser"
]
}
]
},
"HashAlgo": {
"description": "哈希算法。",
"type": "string",
"enum": [
"md5",
"sha1",
"sha256",
"sha512"
]
},
"HashOut": {
"description": "哈希/HMAC 输出编码。",
"type": "string",
"enum": [
"hex",
"base64"
]
},
"HashStep": {
"description": "哈希算子(可选 HMAC)。",
"type": "object",
"required": [
"algo"
],
"properties": {
"algo": {
"$ref": "#/definitions/HashAlgo"
},
"hmacKey": {
"description": "提供则计算 HMAC(以此为密钥)。",
"type": [
"string",
"null"
]
},
"hmacKeyEnc": {
"default": "utf8",
"allOf": [
{
"$ref": "#/definitions/ByteEnc"
}
]
},
"output": {
"default": "hex",
"allOf": [
{
"$ref": "#/definitions/HashOut"
}
]
}
},
"additionalProperties": false
},
"Http": {
"description": "HTTP 配置块。",
"type": "object",
"properties": {
"charset": {
"default": "auto",
"allOf": [
{
"$ref": "#/definitions/Charset"
}
]
},
"cookies": {
"description": "静态 cookie;也是运行时注入 clearance cookie 的落点。",
"default": {},
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"fetcher": {
"description": "取页模式(auto|reqwest|browser);默认 auto。",
"default": "auto",
"allOf": [
{
"$ref": "#/definitions/FetchMode"
}
]
},
"headers": {
"default": {},
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"rateLimit": {
"anyOf": [
{
"$ref": "#/definitions/RateLimit"
},
{
"type": "null"
}
]
},
"retry": {
"anyOf": [
{
"$ref": "#/definitions/Retry"
},
{
"type": "null"
}
]
},
"timeout": {
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
},
"warmup": {
"description": "先 GET 这些页以预热会话 cookie。",
"default": [],
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"LeafRule": {
"description": "叶子规则:在当前上下文做一次抽取。",
"type": "object",
"properties": {
"clean": {
"type": "array",
"items": {
"$ref": "#/definitions/CleanStep"
}
},
"extract": {
"default": "text",
"allOf": [
{
"$ref": "#/definitions/Extract"
}
]
},
"index": {
"type": [
"integer",
"null"
],
"format": "int64"
},
"select": {
"type": [
"string",
"null"
]
},
"via": {
"default": "css",
"allOf": [
{
"$ref": "#/definitions/Via"
}
]
}
}
},
"ListPageSpec": {
"description": "共享列表页规格:search 与 explore 取一页书共用。承载前置链、请求(render / interceptApi / readyFor / totalPages / hasMore / pageBy / vars 均在 [`Request`] 上)、列表选择与每项字段抽取。 引擎以 `run_list_page(spec, vars, page, page_size)` 统一执行。\n\n这正是历史 `SearchOp` 的形状——前序 change 已把全部取页旋钮收敛到 [`Request`],故 search/explore 共用同一规格,无需各维护一套列表求值分支。",
"type": "object",
"required": [
"item",
"list",
"request"
],
"properties": {
"item": {
"$ref": "#/definitions/BookRules"
},
"list": {
"$ref": "#/definitions/Rule"
},
"prelude": {
"description": "主请求之前按序执行的前置请求链(见 design D7-bis);空 = 单发(现状)。",
"type": "array",
"items": {
"$ref": "#/definitions/PreStep"
}
},
"request": {
"$ref": "#/definitions/Request"
}
},
"additionalProperties": false
},
"Method": {
"description": "HTTP 方法。",
"type": "string",
"enum": [
"GET",
"POST"
]
},
"Padding": {
"description": "填充方式(gcm 忽略)。",
"type": "string",
"enum": [
"pkcs7",
"zero",
"none"
]
},
"PageBy": {
"description": "点击驱动翻页策略(`search-click-pagination`):URL 不认页码的 SPA(如番茄 search)翻页只能点 分页器「下一页」,不能靠 `{{page}}` 改 URL。配在可渲染 + 拦截(`render` + `interceptApi`)的 `Request` 上,`page > 1` 时引擎在**一张活页**内点 `click` 选择器 `page-1` 次翻到目标页。 用 `{ click: ... }` 形(留 `by` 扩展位、语义自描述),当前只 `click` 一支。",
"type": "object",
"required": [
"click"
],
"properties": {
"click": {
"description": "「下一页」控件的 CSS 选择器(如番茄 `.byte-pagination .byte-pagination-item-icon:has(.byte-icon-right)`)。",
"type": "string"
}
},
"additionalProperties": false
},
"PreStep": {
"description": "前置请求链中的一步:一个请求 + 其响应上的有序命名捕获(见 design D7-bis)。 本步 url/headers/body 可引用更早步骤捕获的 `{{name}}`。显式列字段(**不**用 `#[serde(flatten)]` 内嵌 [`Request`]:`Rule` 为 untagged 兜底,flatten 会令 `deny_unknown_fields` 校验失效)。",
"type": "object",
"required": [
"url"
],
"properties": {
"body": {
"anyOf": [
{
"$ref": "#/definitions/UrlOrRule"
},
{
"type": "null"
}
]
},
"capture": {
"description": "本步响应上的有序命名捕获(按数组顺序求值;`capture[i]` 可引用 `capture[0..i]`)。",
"type": "array",
"items": {
"$ref": "#/definitions/Capture"
}
},
"headers": {
"description": "请求头(值支持 `{{name}}` 模板,便于带 `Authorization: Bearer {{token}}`)。",
"default": {},
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"method": {
"default": "GET",
"allOf": [
{
"$ref": "#/definitions/Method"
}
]
},
"skipIfPresent": {
"description": "惰性短路:列出的 key 在作用域内**全部非空**则跳过本步(token 复用,避免每章重抓)。",
"type": "array",
"items": {
"type": "string"
}
},
"url": {
"$ref": "#/definitions/UrlOrRule"
}
},
"additionalProperties": false
},
"RateLimit": {
"description": "速率限制。",
"type": "object",
"required": [
"maxCount",
"perMs"
],
"properties": {
"maxCount": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"perMs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
},
"Request": {
"description": "单个请求。",
"type": "object",
"required": [
"url"
],
"properties": {
"body": {
"anyOf": [
{
"$ref": "#/definitions/UrlOrRule"
},
{
"type": "null"
}
]
},
"hasMore": {
"description": "翻页边界规则(`list-has-more`):对取页结果求值「是否还有下一页」(非空且非 `false`/`0` → 有)。 源路由同 `total_pages`(按 `via`)。空 = 不提供边界(UI 不限制)。",
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"headers": {
"default": {},
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"interceptApi": {
"description": "CDP 拦截:渲染时拦截 URL 含此子串的响应体作为取页 body(交 `via:\"json\"` 规则)。 用于「结果只在签名 API、DOM 无关键字段」的站点(典型:番茄搜索 `search_book/v1`)。 可与 `ready_for` 共存(见上)。",
"type": [
"string",
"null"
]
},
"method": {
"default": "GET",
"allOf": [
{
"$ref": "#/definitions/Method"
}
]
},
"pageBy": {
"description": "点击驱动翻页(`search-click-pagination`):URL 不认页码的 SPA(如番茄 search)靠点分页器 「下一页」递增页码。配 `render` + `interceptApi` 且 `page > 1` 时,引擎在一张活页内点 `pageBy.click` 选择器 `page-1` 次翻到目标页、拦该页 API 响应。空 = 现状(单拦截 / `{{page}}` URL 模板翻页),逐字节不变。",
"anyOf": [
{
"$ref": "#/definitions/PageBy"
},
{
"type": "null"
}
]
},
"readyFor": {
"description": "渲染就绪等待选择器。两种用法: - **无 `interceptApi`**:渲染后轮询该选择器出现,取渲染后 DOM 交 CSS 规则(方式 A); - **与 `interceptApi` 共存**(`render-dual-source`):拦 API 取 body 之外,等该选择器出现后 另抓渲染 DOM,供 `via:css` 的 `totalPages` 等对 DOM 求值(如分页器总页数)。",
"type": [
"string",
"null"
]
},
"render": {
"description": "渲染型取页(`render-fetcher`):为真则用受控浏览器渲染本请求 URL、跑站点自身 JS, 而非 reqwest 直取——用于 SPA 站点(结果由 JS 渲染 / 签名 API 返回)。默认 false = 现状。 仅 `browser` feature 且浏览器可用时生效;否则该 op 优雅降级。",
"type": "boolean"
},
"totalPages": {
"description": "精确总页数规则(`render-dual-source`):对取页结果求值得「共 M 页」(search 翻页进度)。 `via:json` 对 API body 求值;`via:css`/`xpath` 对渲染 DOM 求值(需 `interceptApi` + `ready_for` 共存,抓到 DOM 才有源)。空 = 不返回总数(现状)。",
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"url": {
"$ref": "#/definitions/UrlOrRule"
},
"vars": {
"description": "命名捕获:对**本请求响应**每个 `(name, Rule)` 求值,写入 `chapter` 层(等价 `scope=chapter` 的 [`Capture`]),使本 op 的 list/item 与后续步骤以 `{{name}}` 引用。见 design D7-bis。 各条**独立**对响应求值(可引用 `base`/`key`/`page` 与前置 `prelude` 捕获,但**勿互相引用**: 需有序依赖请用 `prelude` 链)。用 `BTreeMap` 保证迭代/落点顺序确定(免哈希随机化幽灵 bug)。",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Rule"
}
}
},
"additionalProperties": false
},
"Retry": {
"description": "重试策略。",
"type": "object",
"properties": {
"backoffMs": {
"default": 0,
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"max": {
"default": 0,
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
},
"RowUi": {
"description": "声明式登录表单的一行(TUI 渲染对应控件,收集值加密存为 loginInfo)。",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "字段名(也是 loginInfo 中的 key)。",
"type": "string"
},
"options": {
"description": "`select` 类型的候选项。",
"type": "array",
"items": {
"type": "string"
}
},
"type": {
"default": "text",
"allOf": [
{
"$ref": "#/definitions/RowUiType"
}
]
}
},
"additionalProperties": false
},
"RowUiType": {
"description": "声明式登录表单项类型。",
"oneOf": [
{
"description": "单行文本。",
"type": "string",
"enum": [
"text"
]
},
{
"description": "密码(TUI 掩码显示)。",
"type": "string",
"enum": [
"password"
]
},
{
"description": "下拉选择(配合 `options`)。",
"type": "string",
"enum": [
"select"
]
},
{
"description": "布尔开关。",
"type": "string",
"enum": [
"toggle"
]
}
]
},
"Rule": {
"description": "一条规则:叶子,或组合子。组合子按其唯一键判别(见 design D1)。\n\n反序列化时按变体顺序尝试:组合子(各有唯一必填键)在前,叶子兜底。",
"anyOf": [
{
"description": "取首个非空子规则结果(回退/自愈)。",
"type": "object",
"required": [
"firstOf"
],
"properties": {
"firstOf": {
"type": "array",
"items": {
"$ref": "#/definitions/Rule"
}
}
}
},
{
"description": "拼接非空子规则结果。",
"type": "object",
"required": [
"concat"
],
"properties": {
"concat": {
"type": "array",
"items": {
"$ref": "#/definitions/Rule"
}
},
"join": {
"default": "",
"type": "string"
}
}
},
{
"description": "字面量。",
"type": "object",
"required": [
"literal"
],
"properties": {
"literal": {
"type": "string"
}
}
},
{
"description": "模板插值(`{{key}}`/`{{page}}`/命名变量)。",
"type": "object",
"required": [
"template"
],
"properties": {
"template": {
"type": "string"
}
}
},
{
"description": "JS 逻辑编排逃生舱(值规则):以当前上下文为 `result`、注入 `baseUrl`/变量 + `crypto` 助手求值,返回字符串。求值需启用 `js` feature(否则返回 `Unsupported(\"js\")`)。 必须置于 `Leaf` 之前——`js` 是其唯一判别键,否则会被全可选的 `Leaf` 吞掉。",
"type": "object",
"required": [
"js"
],
"properties": {
"js": {
"type": "string"
}
}
},
{
"description": "叶子(兜底)。",
"allOf": [
{
"$ref": "#/definitions/LeafRule"
}
]
}
]
},
"Sample": {
"description": "黄金样例。",
"type": "object",
"required": [
"bookUrl"
],
"properties": {
"bookUrl": {
"type": "string"
},
"expect": {
"default": {},
"allOf": [
{
"$ref": "#/definitions/Expect"
}
]
}
},
"additionalProperties": false
},
"StaticEntry": {
"description": "一个静态入口:固定标题 + 取页变量(字面量)。供「全部·最热」这类固定入口声明。",
"type": "object",
"required": [
"title"
],
"properties": {
"title": {
"type": "string"
},
"vars": {
"description": "取页变量:与 base/page/pageSize 合并后运行 `explore.page`。值为字面量。",
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"additionalProperties": false
},
"TocRules": {
"description": "目录规则(章节 + 分卷 + 可选分页)。",
"type": "object",
"required": [
"list",
"name",
"url"
],
"properties": {
"isVolume": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"list": {
"$ref": "#/definitions/Rule"
},
"maxPages": {
"default": 100,
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"name": {
"$ref": "#/definitions/Rule"
},
"nextPage": {
"anyOf": [
{
"$ref": "#/definitions/Rule"
},
{
"type": "null"
}
]
},
"prelude": {
"description": "目录主请求之前按序执行的前置请求链(见 design D7-bis);空 = 现状。",
"type": "array",
"items": {
"$ref": "#/definitions/PreStep"
}
},
"url": {
"$ref": "#/definitions/Rule"
}
},
"additionalProperties": false
},
"UrlOrRule": {
"description": "URL 字段:可为字符串模板,或一条规则。",
"anyOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/Rule"
}
]
},
"VarScope": {
"description": "多步编排:捕获变量的作用域(三级级联 章节→书籍→书源,见 design D7-bis)。 默认 `Chapter`——最短寿命、零持久、零跨书外溢;`get` 时按 章节→书籍→书源 取第一个非空。",
"oneOf": [
{
"description": "仅本次 op 调用(search/explore/bookInfo/toc/content)内存活,调用结束消亡(默认)。 一次性 csrf/sign/cursor 用它。",
"type": "string",
"enum": [
"chapter"
]
},
{
"description": "随 per-book 快照持久化(由 app 注入·导出);如详情/列表 token 复用到 toc/content。 注意:**search/explore 的 `prelude` 阶段尚无 per-book 载体**(用户选书后才建 per-book 引擎), 在那里用 `book` 会写进会被丢弃的实例 → 静默无效;search 阶段的 token 请用 `source`/`chapter`。",
"type": "string",
"enum": [
"book"
]
},
{
"description": "书源级长存(引擎内跨 op 共享);如全站 API host/版本/全站 csrf。 跨会话持久仅在 app 接线落盘时保证(默认构建为进程内)。",
"type": "string",
"enum": [
"source"
]
}
]
},
"Via": {
"description": "抽取后端(决定 `select` 的语义)。",
"oneOf": [
{
"type": "string",
"enum": [
"css",
"xpath",
"json",
"regex"
]
},
{
"description": "直接使用当前上下文值(只跑 clean)。",
"type": "string",
"enum": [
"raw"
]
}
]
}
}
}