Skip to main content

parse_book_source/
lib.rs

1//! `parse-book-source`:AI 原生的结构化书源引擎。
2//!
3//! 书源是一份显式结构化 JSON(无紧凑字符串 DSL),由 [`Engine`] 驱动:
4//! 搜索 / 浏览 / 书详情 / 目录(含分卷)/ 正文,并内置样例校验回路。
5//! 取页经 [`Fetcher`] 端口抽象,默认 [`ReqwestFetcher`]。
6//!
7//! 分层(见 OpenSpec change `ai-friendly-book-source` 的 design):
8//! - `model` — 纯领域类型。
9//! - `source` — v2 配置(serde 镜像 `book-source.schema.json`),其中 `Rule` 既是配置、
10//!   也是供求值器遍历的语法树;按 `rule`/`clean`/`http`/`op` 分文件。
11//! - `eval` — 规则解释器(Interpreter + Composite),含抽取后端 `backend`(css/json/regex/raw)、
12//!   `xpath` 后端、确定性算子 `transform`、JS 逃生舱 `js`。
13//! - `fetch` — 取页端口(Ports & Adapters),含 `cookie` 库与浏览器反爬 `browser`。
14//! - `host` — JS host 桥(`js-host`),含持久状态 `state`。
15//! - `engine` — 用例(search/explore/book_info/toc/content)+ 有界分页。
16//! - `verify` — 样例校验回路。
17//! - `error` — 分层错误。
18
19pub mod engine;
20pub mod error;
21pub mod eval;
22pub mod fetch;
23#[cfg(feature = "js-host")]
24pub mod host;
25pub mod model;
26pub mod source;
27pub mod verify;
28
29// 公开面:运行时入口(Engine)+ 取页端口 + 配置 + 领域类型 + 校验 + 错误。
30// 规则 AST(`Rule` 等)与求值/抽取细节在 `source` / `eval` 下,按需取用。
31pub use engine::Engine;
32pub use error::{BookSourceError, ConfigError, EvalError, FetchError, Result};
33pub use fetch::{FetchRequest, FetchResponse, Fetcher, ReqwestFetcher, is_challenge};
34pub use model::{BookInfo, BookList, BookListItem, Chapter, ExploreEntry, Toc, Volume};
35pub use source::{BookSource, FetchMode, UrlOrRule};
36pub use verify::{Check, CheckStatus, DiagnoseReport, VerifyReport, diagnose, verify_sample};
37
38// 兼容 re-export:聚合后保持历史公开模块路径稳定(主程序/examples 直接引用)。
39// `cookie` 库归入 `fetch/`、`state` 归入 `host/`,这里保留旧顶层路径别名。
40pub use fetch::cookie;
41#[cfg(feature = "js-host")]
42pub use host::state;
43
44// 反爬:系统浏览器解挑战(`browser` feature)。
45#[cfg(feature = "browser")]
46pub use fetch::browser::{
47    AuthDecision, BrowserCookie, BrowserFetcher, BrowserOptions, BrowserUi, Clearance,
48    EscalatingFetcher, LoginCriteria, LoginOutcome, LoginSignal, detect_browser,
49    shutdown_render_pool,
50};
51
52/// 测试共用工具:最小书源 + 一次性本地 HTTP 服务(host / browser 等模块单测共享,
53/// 避免逐处复制 listener / 最小书源样板)。
54#[cfg(test)]
55pub(crate) mod testutil {
56    // 各 feature 组合下使用方不同(host 测试在 js-host、browser 测试在 browser 下),
57    // 允许部分 helper 在某些组合中未被使用。
58    #![allow(dead_code)]
59
60    use crate::source::BookSource;
61    use std::io::{Read, Write};
62    use std::net::TcpListener;
63
64    /// 最小书源(仅为构造取页器:base 指向本地测试服务)。
65    pub(crate) fn book_source(base: &str) -> BookSource {
66        serde_json::from_value(serde_json::json!({
67            "schema": "trnovel-booksource/v2",
68            "name": "t",
69            "url": base,
70            "bookInfo": {},
71            "toc": {"list": {"via": "raw"}, "name": {"via": "raw"}, "url": {"via": "raw"}},
72            "content": {"value": {"via": "raw"}}
73        }))
74        .expect("minimal book source")
75    }
76
77    /// 处理一次连接的回显 HTTP 服务:响应体 = 收到的原始请求(便于断言请求头)。
78    pub(crate) fn spawn_echo_server() -> (String, std::thread::JoinHandle<()>) {
79        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
80        let base = format!("http://{}", listener.local_addr().unwrap());
81        let handle = std::thread::spawn(move || {
82            if let Ok((mut stream, _)) = listener.accept() {
83                let mut buf = [0u8; 8192];
84                let n = stream.read(&mut buf).unwrap_or(0);
85                let body = buf[..n].to_vec();
86                let head = format!(
87                    "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
88                    body.len()
89                );
90                let _ = stream.write_all(head.as_bytes());
91                let _ = stream.write_all(&body);
92                let _ = stream.flush();
93            }
94        });
95        (base, handle)
96    }
97
98    /// 处理一次连接、返回固定原始 HTTP 响应的服务(用于断言响应头/状态码透传)。
99    pub(crate) fn spawn_fixed_server(
100        raw_response: String,
101    ) -> (String, std::thread::JoinHandle<()>) {
102        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
103        let base = format!("http://{}", listener.local_addr().unwrap());
104        let handle = std::thread::spawn(move || {
105            if let Ok((mut stream, _)) = listener.accept() {
106                let mut buf = [0u8; 4096];
107                let _ = stream.read(&mut buf);
108                let _ = stream.write_all(raw_response.as_bytes());
109                let _ = stream.flush();
110            }
111        });
112        (base, handle)
113    }
114}