Files
wiki_crawler/scripts/design.md
2025-12-19 00:52:32 +08:00

234 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
它采用了 **“衔尾蛇 (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秒 超时限制,极其安全。