234 lines
7.1 KiB
Markdown
234 lines
7.1 KiB
Markdown
它采用了 **“衔尾蛇 (Ouroboros)”** 模式:工作流通过 API 自我调用,利用 Dify 的短生命周期特性,实现无限长度的任务队列处理。
|
||
|
||
---
|
||
|
||
# Wiki Crawler RAG - 全自动递归爬虫架构设计文档 V1.2
|
||
|
||
## 1. 概述 (Overview)
|
||
|
||
本项目旨在构建一个基于 URL 锚定的增量式、全自动爬虫系统。
|
||
为了突破 Dify 单次运行的超时限制(Timeout)和内存瓶颈(OOM),本设计采用了 **Map + Scrape + Recursion (递归)** 架构。
|
||
|
||
**核心特性:**
|
||
|
||
* **一次点击,自动托管**:用户仅需输入根 URL,工作流自动完成从发现到入库的全过程。
|
||
* **分批吞噬**:每次运行只处理固定数量(如 50 个)页面,处理完毕后自动触发下一轮运行。
|
||
* **断点续传**:基于数据库状态(Pending/Completed),任何时候中断都可接力继续。
|
||
|
||
---
|
||
|
||
## 2. 架构流程图 (Architecture Diagram)
|
||
|
||
```mermaid
|
||
graph TD
|
||
Start([开始节点<br>Input: url, run_mode]) --> Condition{判断模式<br>run_mode?}
|
||
|
||
%% 分支 A: 初始化模式
|
||
Condition -- "init (默认)" --> Map[HTTP: Firecrawl Map<br>获取全量 URL]
|
||
Map --> InitDB[SQL: init_crawl_queue<br>批量插入 Pending 队列]
|
||
InitDB --> TriggerWorker1[HTTP: Call Self<br>模式切换为 worker]
|
||
TriggerWorker1 --> End1([结束: 初始化完成])
|
||
|
||
%% 分支 B: 打工模式
|
||
Condition -- "worker" --> FetchBatch[SQL: Fetch Pending<br>LIMIT 50]
|
||
FetchBatch --> Iterator[迭代器<br>并发处理 50 个任务]
|
||
|
||
subgraph Iteration Loop
|
||
Iterator --> Scrape[HTTP: Firecrawl Scrape]
|
||
Scrape --> Clean[Python: 清洗 & 提取]
|
||
Clean --> Save[SQL: save_scrape_result<br>入库 & 标记 Completed]
|
||
end
|
||
|
||
Iterator --> CheckLeft[SQL: Count Remaining]
|
||
CheckLeft --> IfLeft{还有剩余吗?<br>Count > 0}
|
||
|
||
IfLeft -- "Yes" --> TriggerWorker2[HTTP: Call Self<br>递归调用 worker]
|
||
TriggerWorker2 --> End2([结束: 本轮批次完成])
|
||
|
||
IfLeft -- "No" --> EndSuccess([结束: 全部爬取完成])
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 数据流动 (Data Flow)
|
||
|
||
### 3.1 状态流转
|
||
|
||
1. **Init 阶段**:`Firecrawl Map` -> `JSON List` -> `DB (crawl_queue)`。此时所有 URL 状态为 `'pending'`。
|
||
2. **Worker 阶段**:`DB (pending)` -> `Dify List` -> `Firecrawl Scrape` -> `DB (knowledge_chunks)` & `DB (status='completed')`。
|
||
|
||
### 3.2 递归逻辑
|
||
|
||
* **Run 1**: `run_mode='init'` -> 发现 1000 个 URL -> 存库 -> 触发 Run 2。
|
||
* **Run 2**: `run_mode='worker'` -> 取前 50 个 -> 抓取 -> 剩余 950 -> 触发 Run 3。
|
||
* **Run ...**: ...
|
||
* **Run 21**: `run_mode='worker'` -> 取最后 50 个 -> 抓取 -> 剩余 0 -> 停止。
|
||
|
||
---
|
||
|
||
## 4. 数据库层准备 (Database Layer)
|
||
|
||
在部署工作流前,必须确保以下 SQL 函数已在 PostgreSQL 中执行。
|
||
|
||
### 4.1 核心表结构 (回顾)
|
||
|
||
* `crawl_tasks`: 存储根任务信息。
|
||
* `crawl_queue`: 存储待爬取 URL 及其状态。
|
||
* `knowledge_chunks`: 存储切片后的文档内容。
|
||
|
||
### 4.2 新增初始化函数 (必需)
|
||
|
||
用于 Map 阶段结束后批量写入队列。
|
||
|
||
```sql
|
||
CREATE OR REPLACE FUNCTION init_crawl_queue(
|
||
p_urls JSONB,
|
||
p_root_url TEXT
|
||
)
|
||
RETURNS VOID AS $$
|
||
BEGIN
|
||
-- 1. 注册/更新主任务
|
||
INSERT INTO crawl_tasks (root_url) VALUES (p_root_url)
|
||
ON CONFLICT (root_url) DO UPDATE SET updated_at = DEFAULT;
|
||
|
||
-- 2. 批量插入待爬取队列 (忽略已存在的)
|
||
INSERT INTO crawl_queue (url, root_url, status)
|
||
SELECT x, p_root_url, 'pending'
|
||
FROM jsonb_array_elements_text(p_urls) AS x
|
||
ON CONFLICT (url) DO NOTHING;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 详细节点定义 (Node Definitions)
|
||
|
||
以下是 Dify 工作流中每个节点的详细配置参数。
|
||
|
||
### 5.1 开始节点 (Start)
|
||
|
||
* **变量 1**: `url` (Text, 必填) - 目标网站 URL。
|
||
* **变量 2**: `run_mode` (Select, 选填) - 运行模式。
|
||
* 选项: `init`, `worker`
|
||
* **默认值**: `init` (保证手动运行时从头开始)
|
||
|
||
### 5.2 逻辑分支 (If-Else)
|
||
|
||
* **条件**: `run_mode` **is** `init`
|
||
* **True 路径**: 进入初始化流程。
|
||
* **False 路径**: 进入打工流程。
|
||
|
||
---
|
||
|
||
### 分支 A:初始化流程 (Init)
|
||
|
||
#### Node A1: HTTP 请求 (Firecrawl Map)
|
||
|
||
* **API**: `POST https://api.firecrawl.dev/v1/map`
|
||
* **Body**:
|
||
```json
|
||
{
|
||
"url": "{{#start.url#}}",
|
||
"limit": 5000,
|
||
"includeSubdomains": true,
|
||
"ignoreSitemap": false
|
||
}
|
||
```
|
||
|
||
#### Node A2: SQL (Init Queue)
|
||
|
||
* **Query**: `SELECT init_crawl_queue($arg0::jsonb, $arg1);`
|
||
* **arg0**: `{{#NodeA1.body.links#}}` (注意:Map 接口返回的是 links 数组)
|
||
* **arg1**: `{{#start.url#}}`
|
||
|
||
#### Node A3: HTTP 请求 (Trigger Self)
|
||
|
||
* **API**: `POST https://api.dify.ai/v1/workflows/run` (替换为您的私有部署域名)
|
||
* **Headers**: `Authorization: Bearer app-xxxxxxxx` (使用本应用的 API Key)
|
||
* **Body**:
|
||
```json
|
||
{
|
||
"inputs": {
|
||
"url": "{{#start.url#}}",
|
||
"run_mode": "worker"
|
||
},
|
||
"response_mode": "blocking",
|
||
"user": "system-recursion-trigger"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 分支 B:打工流程 (Worker)
|
||
|
||
#### Node B1: SQL (Fetch Batch)
|
||
|
||
* **Query**:
|
||
```sql
|
||
SELECT url FROM crawl_queue
|
||
WHERE root_url = $arg0 AND status = 'pending'
|
||
LIMIT 50;
|
||
```
|
||
* **arg0**: `{{#start.url#}}`
|
||
* **Output**: 记为 `batch_list`
|
||
|
||
#### Node B2: 迭代器 (Iterator)
|
||
|
||
* **Input**: `{{#NodeB1.result#}}`
|
||
* **Parallelism**: 开启 (推荐 5-10 并发)
|
||
|
||
> **内部节点 B2-1: HTTP (Scrape)**
|
||
>
|
||
> * API: `POST https://api.firecrawl.dev/v1/scrape`
|
||
> * Body: `{"url": "{{#item.url#}}", "formats": ["markdown"]}`
|
||
>
|
||
> **内部节点 B2-2: Python (Clean)**
|
||
>
|
||
> * Code: 清洗 Markdown,去除图片,截取正文,返回标准 JSON 结构 (含 content, title, url)。
|
||
>
|
||
> **内部节点 B2-3: SQL (Save)**
|
||
>
|
||
> * Query: `SELECT save_scrape_result($arg0::jsonb, $arg1, $arg2);`
|
||
> * arg0: `{{#NodeB2-2.json_string#}}`
|
||
> * arg1: `{{#item.url#}}`
|
||
> * arg2: `{{#start.url#}}`
|
||
>
|
||
|
||
#### Node B3: SQL (Check Remaining)
|
||
|
||
* **Query**:
|
||
```sql
|
||
SELECT count(*) as count FROM crawl_queue
|
||
WHERE root_url = $arg0 AND status = 'pending';
|
||
```
|
||
* **arg0**: `{{#start.url#}}`
|
||
|
||
#### Node B4: 逻辑分支 (Recursion Check)
|
||
|
||
* **条件**: `{{#NodeB3.result[0].count#}}` **>** `0`
|
||
|
||
#### Node B5: HTTP 请求 (Trigger Self - Recursion)
|
||
|
||
* *(配置同 Node A3)*
|
||
* **作用**: 当检测到还有剩余任务时,再次调用自己,开启下一轮 50 个页面的抓取。
|
||
|
||
---
|
||
|
||
## 6. 异常处理与安全机制
|
||
|
||
1. **死循环熔断**:
|
||
|
||
* 建议在 HTTP Trigger Body 中增加一个 `loop_count` 字段。
|
||
* `inputs: { "loop_count": {{#start.loop_count#}} + 1 }`
|
||
* 在 Start 节点后增加校验:如果 `loop_count > 100`,强制停止,防止意外消耗过多额度。
|
||
2. **API Rate Limit**:
|
||
|
||
* 如果 Firecrawl 报错 429,迭代器内的 SQL 节点不会执行 `UPDATE ... SET completed`。
|
||
* 该 URL 状态仍为 `pending`。
|
||
* 下一轮 Worker 运行时,会再次尝试抓取该 URL(自动重试机制)。
|
||
3. **超时控制**:
|
||
|
||
* 每个 Worker 批次处理 50 个页面,以单页 5秒计算,并发 10的情况下,耗时约 25-30秒。
|
||
* 远低于 Dify 默认的 300秒/600秒 超时限制,极其安全。
|