它采用了 **“衔尾蛇 (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([开始节点
Input: url, run_mode]) --> Condition{判断模式
run_mode?} %% 分支 A: 初始化模式 Condition -- "init (默认)" --> Map[HTTP: Firecrawl Map
获取全量 URL] Map --> InitDB[SQL: init_crawl_queue
批量插入 Pending 队列] InitDB --> TriggerWorker1[HTTP: Call Self
模式切换为 worker] TriggerWorker1 --> End1([结束: 初始化完成]) %% 分支 B: 打工模式 Condition -- "worker" --> FetchBatch[SQL: Fetch Pending
LIMIT 50] FetchBatch --> Iterator[迭代器
并发处理 50 个任务] subgraph Iteration Loop Iterator --> Scrape[HTTP: Firecrawl Scrape] Scrape --> Clean[Python: 清洗 & 提取] Clean --> Save[SQL: save_scrape_result
入库 & 标记 Completed] end Iterator --> CheckLeft[SQL: Count Remaining] CheckLeft --> IfLeft{还有剩余吗?
Count > 0} IfLeft -- "Yes" --> TriggerWorker2[HTTP: Call Self
递归调用 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秒 超时限制,极其安全。