yexiyue/trnovel/booksource-generator

Claude Code and agent skill by yexiyue/trnovel

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

从一个小说网站 URL 生成 TRNovel 书源(trnovel-booksource/v2 JSON)。当用户给出小说/阅读类网站并希望接入 TRNovel、做书源、适配书源、逆向某站点目录/正文时使用——例如「给这个站做个书源 https://…」「generate a book source for X」「把这个小说网站接入 trnovel」「帮我解析这个站的章节/搜索」,即便没明说「书源」二字。 本 skill 会探测真实站点、逆向 search/浏览/书详情/目录(含分卷)/正文 的 CSS/JSONPath 选择器,处理字符集与 Cloudflare 反爬,写出 JSON,并用 `trn doctor` 全流程校验、按 ✓/✗ 迭代到通过。

Install

npx vskill@latest install yexiyue/trnovel/booksource-generator

SKILL.md

书源生成器(TRNovel v2)

这是什么 · 书源的本质

书源是一份声明式 JSON,教 TRNovel 如何读一个小说站:去哪搜索、怎么浏览分类、从书详情页抽取书名/作者/封面、列出目录(章节 + 分卷)、抓取每章正文。你的任务:给定站点 URL,逆向它的 HTML,产出一份能通过 `trn doctor` 的 `trnovel-booksource/v2` JSON。

格式是结构化的(没有紧凑字符串 DSL):每个字段是一条「规则」——一次 CSS/JSONPath/正则抽取,或组合子(firstOf/concat/literal/template)。

  • 精确字段与规则语法 → 读 references/book-source.schema.json
  • 一份完整可运行的真实示例 → 读 references/example-bilixs.v2.json(MacCMS 系静态站,含 fetch 动态入口)
  • 另一份真实示例(番茄小说网页版,SPA + 签名接口 + 渲染型取页) → 读 references/example-fanqie.v2.json
  • 逐能力的探站/逆向战术、curl 探测配方、字符集与反爬细节 → 读 references/reverse-engineering.md

边做边查这三个文件,不要凭记忆硬编规则语法。

核心循环(照做)

  1. 探站:抓取代表性页面——首页、一本书的详情页、它的目录页、一个搜索结果页、一个分类浏览页。用 curl/WebFetch,保存 HTML,观察结构。
  2. 逐能力逆向选择器:search / explore / bookInfo / toc / content(见下「按能力逆向」)。
  3. 判定字符集 + 反爬(见下两节)。
  4. 拼装 JSON(schema: "trnovel-booksource/v2",带 samples 黄金样例)。
  5. 校验:`trn doctor <文件>` → 看逐项 ✓/✗ → 只修那个 ✗ 对应的规则 → 重跑,直到全绿(或只剩已知难项,如被挑战的搜索)。
  6. 导入:`trn import <文件>` —— 把验证通过的书源写入 ~/.trnovel/data/,使其在网络小说里立即可用(闭环的最后一步,见下「导入」)。
  7. 告知用户:doctor 各项结果 + 已导入,可在 trn -n 选用。

绝不把未经 doctor 校验的书源交付,也别忘了最后一步导入。 校验循环把「猜的选择器」变成「验证过的选择器」;导入让它真正能用。

用 doctor 做反馈引擎

在仓库根目录:

bash
cargo run -- doctor /path/to/<站点>.v2.json
# 或已安装的二进制:trn doctor <文件>

它跑完整流程并对 配置 / 浏览 / 书详情 / 目录 / 正文 / 搜索 打 ✓/✗/○。读每个 ✗ 的 detail,一次只修一条规则,再跑。无样例时它会用浏览结果探一本书来测读取链路,所以先把 `explore` 或 `samples` 做对能解锁后面几项。

迭代提速:可先 cargo build 一次,然后反复跑 ./target/debug/trn doctor <文件>(见 scripts/validate.sh)。

导入(闭环最后一步)

doctor 验证通过后,把书源装进 app 让它真正可用:

bash
cargo run -- import /path/to/<站点>.v2.json
# 或已安装的二进制:trn import <文件> (也支持 URL:trn import https://…/source.json)

它把书源写入 ~/.trnovel/data/book_sources.json(按 url+name 去重,同名会被新的覆盖,便于反复迭代同一书源),之后在网络小说(trn -n)的书源列表里即可选用。这是「AI 生成 → 验证 → 可用」闭环的收尾:不导入,生成的书源只是个文件,用户用不上。

按能力逆向(关键启发式)

每条规则的 select 是 self-or-descendant 语义;HTML 默认 via: "css"。下面是从真实站点(MXCMS/MacCMS 系等)总结的高命中套路,细节见 references/reverse-engineering.md。

  • bookInfo(书详情):先找 `og:` meta——<meta property="og:novel:book_name">、og:novel:author、og:novel:read_url(常是 tocUrl)、og:image(封面)、og:description。用 {"via":"css","select":"[property=\"og:novel:book_name\"]","extract":{"attr":"content"}}。比靠页面可见元素稳得多。`wordCount`(字数)通常不在 og: meta 里,需从页面可见元素提取(如 span.tag-link 含 "878.7 万字");MacCMS 系站点的字数常在书详情页的辅助信息区。
  • toc(目录 + 分卷):目录页里卷标题(如 第一卷)和章节链接通常是兄弟节点。用一个 list 选择器同时选中两者并保持文档顺序(如 .box > h2.module-title, .box a.module-row-text);再用 isVolume 规则判定是否卷(对卷节点非空、对章节为空,例如选 h2);name/url 用 firstOf 兼容两种节点。引擎据 isVolume 切分「卷→章」。常见坑:目录开头常有 N 条「最新章节」预览(倒序、与正文区重复),用兄弟选择器(如 #list dl dt:has(a[href*="txt_"]) ~ dd a)只取「正文」标记之后的章节,避免重复/倒序。
  • content(正文):找正文容器(.article-content/#content/.read-content 等),extract: "html" 会把标签转成换行后清理;分页正文用 nextPage + maxPages。若正文夹大量私有区/豆腐字符(`U+E000`–`U+F8FF`,显示为豆腐 □)→ 是字体反爬,见下「字体反爬」。
  • search(搜索):从首页 <form> 找 action 与 input name(如 searchkey/wd)。request.url 用模板 {{base}}/search.html?searchkey={{key}};list 选结果条目容器(注意区分真正的结果区与推荐位——它们可能用不同 class);item 里必须含 `bookUrl`(指向书详情)。POST 搜索用 method:"POST" + body 模板。
  • explore(浏览):两阶段结构——entries 生成可选入口,page 用选中入口的变量取书。
  • `entries`:入口源数组,两种变体:
  • static:硬编码入口列表 [{title, vars?}]。vars 是取页变量(字面量),与 base/page/pageSize 合并后驱动 page。
  • fetch:从远端页面动态抓取入口 {request, list, item}。request 取入口数据页,list 选数组,item 提取 title(Rule) + vars({name: Rule})。可选 forEach 按多组变量重复请求。
  • 优先用 `fetch` 动态提取分类入口(从首页导航/API 获取),而非 static 硬编码。这样站点新增分类时不用手动更新书源。仅当站点无可用分类入口时才用 static。
  • 两种可混用,按声明顺序合并。
  • `page`:共享列表页规格 {request, list, item}——request.url 用入口变量 + {{page}} 模板生成取页 URL;list/item 同 search。
  • `fetch` 入口的坑:vars 里的 Rule 对「当前数据项」求值。dom_query 解析 HTML 片段时会包 <html><body>,省略 `select` 时 `:root` 指向 `<html>` 而非目标元素。提取 href 等属性时必须显式写 `select`(如 "select": "a")匹配目标元素自身。
  • 过滤非目标入口:list 选择器应尽量精确(如 .nav-menu-items a[href*='lastupdate'] 而非 a),避免匹配到无关链接导致 vars 提取失败、拼出错误 URL。
  • SPA 书库站(分类列表由签名接口拉、无静态分类页):在 page.request 上加 render:true + interceptApi,list/item 改 via:"json" 对齐被拦截 API。取页单页:{{page}} 映射 URL(从 1 起、由 URL 模板对齐站点 API 页码基数,引擎不内置偏移),交互翻页由阅读器递增 page 驱动(列表不加 nextPage)。详见 references/reverse-engineering.md「渲染 explore」。

{{base}} = 站点根、{{key}} = 搜索词、{{page}}/{{pageSize}} = 分页。相对 URL 引擎会自动补全,抽取 href 时保留相对路径即可。

字符集

老站常是 GBK。若抓下的中文乱码,设 http.charset: "gbk"(或 gb18030)。不确定就用默认 auto(UTF-8 失败回退 GBK),doctor 正文/书名出现乱码即调。

反爬(Cloudflare 等)

若某端点(常是搜索)返回挑战页,doctor 会报「被反爬挑战拦截,需浏览器辅助或改用浏览」。处理顺序:

  1. 先找未被挑战的等价入口:很多站只锁伪静态搜索路径,底层路由(如 /index.php/vod/search.html?...)或 JSON 接口未被挡。换上去就不用反爬。但要核实它真在按关键词过滤(换两个词对比结果),别被「不管查啥都返回同一榜单」的假入口骗了。
  2. 绕不开就标注取页模式:站点整体能读、仅搜索被挑战 → http.fetcher: "auto"(默认,平时 reqwest、撞挑战才用系统浏览器解 cf_clearance);整站首请求即被挑战 / 正文靠 JS 渲染 → "browser";想永不开浏览器 → "reqwest"(撞挑战即降级)。
  3. 判「不可行」前先试低成本绕过(很多站的拦截是浅的):
  • 章节页返回「加载中…」的 JS 跳转桩(HTTP 200,不是 CF 挑战,fetcher:browser 救不了)?试在 URL 后加个无害 query(如 ?nocache=1,用 clean.append)常能直接绕到真正文。
  • 正文/搜索是 token 门控的 JSON 接口?把发 token 的辅助端点(如 /user/hm.html)加进 http.warmup 拿 cookie,再用 via:"json" 解接口(cookie_store 会跨请求保留)。
  1. 接受降级:阅读链路通常开放,搜索被挑战不影响读书。doctor 把搜索标 ✗ 但 detail 精确,即视为「需浏览器/改用浏览」,不是规则 bug。

架构边界:若整页正文由站点自身 JS 异步渲染 / 加密接口动态拉取(没有任何静态路径返回正文文本),当前 reqwest + cookie 烤箱拿不到,fetcher:browser 也只解 CF 挑战、不跑站点内容 JS。此时如实告知用户「该站正文需能执行页面 JS 的渲染型取页(暂不支持)」,把能做的(浏览/书详情/目录)做好并保留正文选择器兜底。

详见 references/reverse-engineering.md 的「反爬」一节。

字体反爬(自定义字体 + PUA 占位)

有些站(典型:番茄小说网页版)正文里夹大量私有区字符(U+E000–U+F8FF,多显示为豆腐方块 □),靠页面一套自定义 @font-face 字体在浏览器里渲染回真字——你抓到的文本是 PUA 码点,直接读是乱码。这不是选择器问题,是要「还原」。

识别:抓到的正文/书名含大量私有区字符;页面 CSS 里有 @font-face { … src:url(….woff2) }。

还原(两步):

  1. 找到加密字体 URL(页面 CSS 的 @font-face),生成「码点 → 真字」映射表:

``bash trn gen-fontmap <字体URL或本地woff2> -o fontmap.json # 基准字体缺省自动下载 Noto ` 纯 Rust 字形匹配(woff2 解压 + 渲染字形 + 和常用汉字/数字/字母比长相),输出 {码点:真字}`,并列出低置信字供人工核对。数字 0-9、英文字母也会被混淆进 PUA,候选集已覆盖、能正确还原(旧版工具只有汉字候选,会把「q」等解成形近汉字)。

  1. 把表内联进正文规则的 clean:

``json "content": { "value": { "via": "css", "select": ".content", "extract": "html", "clean": [ { "fontMap": { "E4DE": "一", "E4F3": "的" } } ] } } ``

要点:同一站的正文、搜索和书库也可能使用不同字体;分别取各页面实际加载的字体生成映射,不能跨页面复用(番茄示例有 content、search、explore 三张表)。gen-fontmap 的相似度匹配可能把形近字误认,即使没有低置信提示,也要用实际正文或列表语境核对可疑字(如「眼」被识别为「限」)。引擎只按表查、不内置任何站点的表(表是数据、跟着书源走)。详解见 references/reverse-engineering.md 的「字体反爬」与博客 dev-notes/blog/font-anti-scraping-and-fontmap.md。

登录与多步编排(host 桥,需 js-host 构建)

需要登录/会员才能读全本的站(典型:番茄网页版会员、JWT 鉴权书站),靠书源 JS 里的有状态 host 对象完成登录与跨请求传值。注入三个语义对象(不是 Legado 的 java):

  • source —— 书源状态/登录:put/get(跨请求 KV)、getVariable/putVariable、putLoginHeader/getLoginHeader/getLoginHeaderMap/removeLoginHeader、getLoginInfo/getLoginInfoMap/putLoginInfo(凭据 AES 加密存)。
  • net —— 网络/cookie:ajax(url)→响应体、connect(url, extraHeadersJson?)/post(url, body, extraHeadersJson?)→{body, code, headers}、getCookie(domain, key?)。
  • crypto —— 沿用(md5/sha/aes/base64/hex/t2s/s2t)。

登录态统一:source.putLoginHeader(json) 存任意 header map,引擎每请求自动并入——Authorization: Bearer <jwt>、自定义 token 头、Cookie 三者同一条路径(无需 JWT 专门逻辑)。含 Cookie 字段会同步进 cookie 库(按注册域)。

新增书源字段:

字段作用
loginUrl普通 URL(走浏览器登录),或 @js:…/<js>…</js> 登录脚本(内含 login() 函数)
loginUi声明式登录表单 [{name,type}],type ∈ text/password/select/toggle;收集值加密存 loginInfo
loginCheckJs每个网络方法响应后执行(result=响应),返回空/false/0 判失效 → 提示重登
enabledCookieJar开启后响应 Set-Cookie 自动回灌 cookie 库
concurrentRate限速 "N/ms" 或纯毫秒间隔
prelude(各 op)结构化前置请求链 [{url,method?,body?,headers?,capture,skipIfPresent?}],主请求/分页前按序执行;默认构建即可用,不需 js-host

脚本登录范式(loginUrl 为脚本):约定导出全局 login(),内部取凭据→发请求→写回登录态:

js
// loginUrl: "@js:function login(){ var r = net.post('/api/login', JSON.stringify(JSON.parse(source.getLoginInfo()))); var tok = JSON.parse(r.body).token; source.putLoginHeader(JSON.stringify({Authorization:'Bearer '+tok})); }"

结构化多步(声明式,首选;不需 js-host):每个 op(search/explore/bookInfo/toc/content)可挂 prelude 前置请求链,每步对其响应做命名 capture,后续步骤/抽取规则用 {{name}} 引用——不写字符串 DSL(优于 host JS 的 source.put/get 命令式)。

jsonc
"toc": {
"prelude": [
{ "url": {"template":"{{base}}/api/prepare"},
"capture": [{ "name":"csrf", "value": {"via":"json","select":"$.csrf"}, "scope":"chapter" }],
"skipIfPresent": ["csrf"] } // 已有则跳过,避免每章重抓
],
"list": { "...": "主请求/规则用 {{csrf}} 引用" }
}
  • scope:chapter(默认,本次调用消亡,一次性 csrf/sign/cursor 用它)/ book(随本书快照,详情→目录→正文复用)/ source(书源级,站级常量;同会话跨 op 共享)。
  • 捕获先于引用由数组顺序保证;捕获产物是字符串;搜索主请求的 request.vars({name:Rule})等价对搜索响应的 chapter 级捕获,供 list/item 引用。
  • 跨会话/历史续读是全新引擎:需 token 的 toc/content 应各自声明 prelude + skipIfPresent 幂等重取(search 阶段 token 用 source/chapter,别用 `book`——选书前无本书快照)。

host JS 多步(需 `js-host`):JS 内 net.ajax/connect 复用取页管线(自动带 loginHeader + 按注册域的库 cookie);命令式跨请求传值用 source.put/get。不引入 Legado 的 @put:/@get: 字符串 DSL。

要点:prelude/capture 结构化多步默认构建即可用;loginUrl/net.*/source.* 等登录与 host 网络能力仅 js-host 构建可用,纯净构建(默认)无网络/状态、行为不变。站点是否需登录用 AskUserQuestion 问用户,再决定 loginUrl/loginUi 形态。

拿不准就问(用 AskUserQuestion)

探站时遇到你无法从页面自行判定的事,别瞎猜——用 AskUserQuestion 问用户。典型:

  • 站点需要登录/会员才能看正文或目录;
  • 有多个候选区块,分不清哪个是「正文/书单」主体;
  • 没有可用的样例书(samples 需要一个真实 book_url + 期望);
  • 站点疑似需要浏览器才能拿到内容,问是否启用 fetcher: "browser";
  • 同一意图有多个搜索入口,问优先哪个。

把可推断的(默认 charset、相对 URL、auto fetcher)直接定下来,只把真正需要用户拍板的拿出来问。

产物

  • <站点>.v2.json:schema/name/url/http?/search?/explore?/bookInfo/toc/content/samples。
  • 至少一条 samples(一个真实 bookUrl + expect,如 name、minChapters、volumes、minContentChars)——既驱动 doctor 在无浏览时也能验,也供运行期自愈。
  • 一句话交付小结:doctor 各项结果、哪些 ✓、哪些是已知降级项(如被挑战的搜索)。

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
78
Repository forks
13
7-day trend score
0

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/yexiyue/trnovel/booksource-generator/badge)](https://verified-skill.com/skills/yexiyue/trnovel/booksource-generator)

Something wrong with this skill or its page? Tell us.