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