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

7.1 KiB
Raw Blame History

它采用了 “衔尾蛇 (Ouroboros)” 模式:工作流通过 API 自我调用,利用 Dify 的短生命周期特性,实现无限长度的任务队列处理。


Wiki Crawler RAG - 全自动递归爬虫架构设计文档 V1.2

1. 概述 (Overview)

本项目旨在构建一个基于 URL 锚定的增量式、全自动爬虫系统。 为了突破 Dify 单次运行的超时限制Timeout和内存瓶颈OOM本设计采用了 Map + Scrape + Recursion (递归) 架构。

核心特性:

  • 一次点击,自动托管:用户仅需输入根 URL工作流自动完成从发现到入库的全过程。
  • 分批吞噬:每次运行只处理固定数量(如 50 个)页面,处理完毕后自动触发下一轮运行。
  • 断点续传基于数据库状态Pending/Completed任何时候中断都可接力继续。

2. 架构流程图 (Architecture Diagram)

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 阶段结束后批量写入队列。

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:
    {
      "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:
    {
      "inputs": {
        "url": "{{#start.url#}}",
        "run_mode": "worker"
      },
      "response_mode": "blocking",
      "user": "system-recursion-trigger"
    }
    

分支 B打工流程 (Worker)

Node B1: SQL (Fetch Batch)

  • Query:
    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:
    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秒 超时限制,极其安全。