{
"$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/BookRules"
},
"content": {
"$ref": "#/definitions/ContentRules"
},
"explore": {
"anyOf": [
{
"$ref": "#/definitions/ExploreOp"
},
{
"type": "null"
}
]
},
"group": {
"type": "string"
},
"http": {
"default": {
"charset": "auto",
"cookies": {},
"fetcher": "auto",
"headers": {},
"warmup": []
},
"allOf": [
{
"$ref": "#/definitions/Http"
}
]
},
"name": {
"type": "string"
},
"samples": {
"type": "array",
"items": {
"$ref": "#/definitions/Sample"
}
},
"schema": {
"description": "固定为 `\"trnovel-booksource/v2\"`。",
"type": "string"
},
"search": {
"anyOf": [
{
"$ref": "#/definitions/SearchOp"
},
{
"type": "null"
}
]
},
"toc": {
"$ref": "#/definitions/TocRules"
},
"url": {
"description": "站点基址,用于相对链接解析与 `{{base}}`。",
"type": "string"
}
},
"additionalProperties": false,
"definitions": {
"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"
]
}
]
},
"Category": {
"description": "浏览分类。",
"type": "object",
"required": [
"title",
"url"
],
"properties": {
"title": {
"type": "string"
},
"url": {
"$ref": "#/definitions/UrlOrRule"
}
},
"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"
}
]
},
"value": {
"$ref": "#/definitions/Rule"
}
},
"additionalProperties": false
},
"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": "浏览操作。",
"type": "object",
"required": [
"categories",
"item",
"list"
],
"properties": {
"categories": {
"type": "array",
"items": {
"$ref": "#/definitions/Category"
}
},
"item": {
"$ref": "#/definitions/BookRules"
},
"list": {
"$ref": "#/definitions/Rule"
}
},
"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"
]
},
"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"
}
]
}
}
},
"Method": {
"description": "HTTP 方法。",
"type": "string",
"enum": [
"GET",
"POST"
]
},
"Padding": {
"description": "填充方式(gcm 忽略)。",
"type": "string",
"enum": [
"pkcs7",
"zero",
"none"
]
},
"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"
}
]
},
"headers": {
"default": {},
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"method": {
"default": "GET",
"allOf": [
{
"$ref": "#/definitions/Method"
}
]
},
"url": {
"$ref": "#/definitions/UrlOrRule"
},
"vars": {
"description": "命名捕获,供 template 使用。",
"default": {},
"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
},
"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
},
"SearchOp": {
"description": "搜索操作。",
"type": "object",
"required": [
"item",
"list",
"request"
],
"properties": {
"item": {
"$ref": "#/definitions/BookRules"
},
"list": {
"$ref": "#/definitions/Rule"
},
"request": {
"$ref": "#/definitions/Request"
}
},
"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"
}
]
},
"url": {
"$ref": "#/definitions/Rule"
}
},
"additionalProperties": false
},
"UrlOrRule": {
"description": "URL 字段:可为字符串模板,或一条规则。",
"anyOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/Rule"
}
]
},
"Via": {
"description": "抽取后端(决定 `select` 的语义)。",
"oneOf": [
{
"type": "string",
"enum": [
"css",
"xpath",
"json",
"regex"
]
},
{
"description": "直接使用当前上下文值(只跑 clean)。",
"type": "string",
"enum": [
"raw"
]
}
]
}
}
}