Skip to main content

pagesift_api/
engine.rs

1//! 抓取引擎接口。API 层只做协议、校验和任务管理,真正的抓取由实现这个 trait 的引擎完成。
2
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use crate::error::ApiError;
8use crate::types::{
9    CrawlRequest, CrawlSummary, Document, ExtractRequest, MapLink, MapRequest, ScrapeOptions,
10    SearchRequest, Upload,
11};
12use serde_json::Value;
13
14pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
15
16pub trait Engine: Send + Sync + 'static {
17    fn scrape<'a>(
18        &'a self,
19        url: &'a str,
20        opts: &'a ScrapeOptions,
21    ) -> BoxFuture<'a, Result<Document, ApiError>>;
22
23    /// `/v2/parse`:解析调用方上传的文件(PDF、HTML),返回和 scrape 相同的文档。
24    /// `opts` 已经过 [`crate::check_parse_options`] 校验。
25    fn parse<'a>(
26        &'a self,
27        _file: Upload,
28        _opts: &'a ScrapeOptions,
29    ) -> BoxFuture<'a, Result<Document, ApiError>> {
30        Box::pin(async {
31            Err(ApiError::unsupported(
32                "parse is not available in this build",
33            ))
34        })
35    }
36
37    fn map<'a>(&'a self, _req: &'a MapRequest) -> BoxFuture<'a, Result<Vec<MapLink>, ApiError>> {
38        Box::pin(async { Err(ApiError::unsupported("map is not available in this build")) })
39    }
40
41    /// 整站爬取:抓到一页、出错一页、发现新 URL 时都通过 `sink` 回报,直到爬完或被取消。
42    fn crawl<'a>(
43        &'a self,
44        _req: &'a CrawlRequest,
45        _sink: Arc<dyn CrawlSink>,
46    ) -> BoxFuture<'a, Result<(), ApiError>> {
47        Box::pin(async {
48            Err(ApiError::unsupported(
49                "crawl is not available in this build",
50            ))
51        })
52    }
53
54    /// `/v2/search`:返回 Firecrawl 的 `data`(`{ "web": [...] }`)。
55    fn search<'a>(&'a self, _req: &'a SearchRequest) -> BoxFuture<'a, Result<Value, ApiError>> {
56        Box::pin(async {
57            Err(ApiError::unsupported(
58                "search is not configured: set [search] in pagesift.toml",
59            ))
60        })
61    }
62
63    /// `/v2/extract`:返回提取出的 JSON。
64    fn extract<'a>(&'a self, _req: &'a ExtractRequest) -> BoxFuture<'a, Result<Value, ApiError>> {
65        Box::pin(async {
66            Err(ApiError::unsupported(
67                "extract needs an LLM: set [llm] in pagesift.toml",
68            ))
69        })
70    }
71
72    /// 引擎支持的 `formats`;请求了不支持的格式时返回 UNSUPPORTED_OPTION,不悄悄忽略。
73    fn supported_formats(&self) -> &'static [&'static str] {
74        &["markdown", "html", "rawHtml", "links", "images"]
75    }
76
77    /// 是否支持 `actions`(需要浏览器渲染)。
78    fn supports_actions(&self) -> bool {
79        false
80    }
81}
82
83/// 整站爬取的进度回报,由 API 层的任务管理实现。
84pub trait CrawlSink: Send + Sync {
85    fn page(&self, doc: Document) -> BoxFuture<'_, ()>;
86    fn error<'a>(&'a self, url: &'a str, err: ApiError) -> BoxFuture<'a, ()>;
87    /// 已经确定要抓的页面总数(随着发现新链接增长)。
88    fn total(&self, total: usize) -> BoxFuture<'_, ()>;
89    fn cancelled(&self) -> BoxFuture<'_, bool>;
90
91    /// 断点续爬:任务是从中断处恢复的时候返回上次的进度,引擎据此重建队列和去重表,
92    /// 不再抓已经完成的页面。新任务返回 `None`。
93    fn resume(&self) -> Option<CrawlResume> {
94        None
95    }
96
97    /// 是否需要 [`CrawlSink::enqueued`] 回报(持久化的任务需要)。不需要时引擎不必收集。
98    fn tracks_frontier(&self) -> bool {
99        false
100    }
101
102    /// 新进入队列的 URL 和发现深度,按入队顺序。只在 [`CrawlSink::tracks_frontier`] 为真时调用。
103    fn enqueued(&self, _urls: Vec<(String, usize)>) -> BoxFuture<'_, ()> {
104        Box::pin(async {})
105    }
106
107    /// 爬取结束时的说明(停止原因等);只有带查询的爬取会调用。
108    fn summary(&self, _summary: CrawlSummary) -> BoxFuture<'_, ()> {
109        Box::pin(async {})
110    }
111}
112
113/// 中断的爬取任务恢复时的进度。完成的页面以文档的 `metadata.sourceURL`(失败的以出错的 URL)为准。
114#[derive(Debug, Clone, Default, PartialEq)]
115pub struct CrawlResume {
116    /// 入过队的全部 URL,按入队顺序(去重表)。
117    pub enqueued: Vec<String>,
118    /// 入了队但还没完成的 URL 和发现深度,按入队顺序。中断时正在抓的页面也在这里。
119    pub pending: Vec<(String, usize)>,
120    /// 已经完成的页数(成功和失败都算),计入 `limit`。
121    pub done: usize,
122}