Files
wiki_crawler/scripts/design.md

234 lines
7.1 KiB
Markdown
Raw Normal View History

2025-12-19 00:52:32 +08:00
它采用了 **“衔尾蛇 (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秒 超时限制,极其安全。