{
  "$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"
          ]
        }
      ]
    }
  }
}
