# dianshan 插件爬取任务开发文档(逻辑/通信/状态) > 目的:把“点击爬取后发生了什么、会触发哪些方法、给谁通信、改变哪些状态”说清楚,方便后续二开与排查。 > > 范围:`dianshan/` 目录内的扩展(popup + background + content/overlay + 外部网页桥接)。 > > 注意:当前项目代码里没有 `startDianshanCrawl()` / `getDianshanCrawlState()` 这类方法名; > 正确入口分别是 `startCrawl()`(`dianshan/src/background/task/crawlTask.ts:14`)与 `getCrawlTaskState()`(`dianshan/src/background/task/taskState.ts:10`),以及对应的消息 action(`START_CRAWL` / `GET_CRAWL_STATE`)。 --- ## 1. 关键文件索引(按职责) ### 1.1 Popup(扩展弹窗 UI) - `dianshan/src/popup/App.vue` - UI 展示:平台选择、开始/取消/继续按钮、步骤列表、底部语言切换/版本号 - `dianshan/src/popup/hook/use-scan.ts` - 与 background 通信(开始/取消/继续/读取状态) - 监听 `chrome.storage.onChanged` 同步任务状态 - 计时器:根据 `startedAt` 计算已用时 - `dianshan/src/popup/hook/use-i18n.ts` - Popup 文案 i18n(中文/英文),持久化到 `chrome.storage.local` ### 1.2 Background(后台调度 + 任务状态机) - `dianshan/src/background/index.ts` - 统一接收 popup/content/external 的 message(`chrome.runtime.onMessage`) - 监听窗口/Tab 关闭(自动暂停任务) - 监听 storage 变化并对外广播(external bridge) - `dianshan/src/background/task/crawlTask.ts` - 任务生命周期:start/cancel/pause/resume/dismiss - 步骤执行器:按 steps 顺序打开页面、等待、抓取、处理中断/重试 - 完成后收尾:发送结果 -> 清空记录 -> 关闭窗口 - `dianshan/src/background/task/taskState.ts` - 读写 `chrome.storage.local` 的 `crawlTaskState` - 写入时同时 `sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)` 推送给爬取窗口(overlay 用) - `dianshan/src/background/task/helper.ts` - `openSingleTabWindow()` 创建 popup 类型新窗口并返回 `{windowId, tabId}` - `waitForTabLoaded()` 等待 tab load complete - `scrapeStepInContent()` 与 content script 通信执行抓取(`tabs.sendMessage`) - `dianshan/src/background/service/externalBridge.ts` - 与外部网页(官网/业务系统)进行 external message / long-connect 通信 - 监听 storage 变化,广播 `DIANSHAN_CRAWL_STATE / DONE / ...` 等事件 ### 1.3 Content Script(爬取窗口页内逻辑) - `dianshan/src/content/pageRunner.ts` - 接收 background 指令: - `CHECK_INTERRUPT`:判断登录/验证码/404/未就绪等“需要人工处理”的中断 - `SCRAPE_STEP`:等待关键 selector 稳定后执行 DOM 抓取 - `dianshan/src/content/crawlOverlay.ts` - 爬取窗口左下角悬浮 UI(圆形计时菜单) - 点击圆形菜单展开后展示“与 popup 类似”的步骤进度/暂停提示/继续/取消 - 接收 background 推送的 `CRAWL_STATE_UPDATE` 状态 - 初始化时会拉取一次 `GET_CRAWL_STATE_FOR_TAB` 快照(只在爬取 tab 返回状态) ### 1.4 Shared(跨模块协议) - `dianshan/src/shared/message.ts` - popup/content -> background 的消息 action 类型(例如 `START_CRAWL`) - `dianshan/src/shared/tab.ts` - background -> content(爬取窗口 tab)消息 action 类型(例如 `CRAWL_STATE_UPDATE`) - `dianshan/src/types/crawl.ts` - `CrawlTaskState` / `CrawlProgressStep` / `CrawlPauseInfo` 等任务数据结构 ### 1.5 关键方法定位(按真实代码) > 下面列的都是当前项目里“确实存在”的方法/入口,后面章节会反复引用;每条都附带文件地址+行号,方便你 Ctrl+P 直达。 - popup 启动爬取:`handleScan()`:`dianshan/src/popup/hook/use-scan.ts:46` - popup 取消爬取:`handleCancelCrawl()`:`dianshan/src/popup/hook/use-scan.ts:70` - popup 继续爬取:`handleResumeCrawl()`:`dianshan/src/popup/hook/use-scan.ts:83` - popup 同步状态:`handleStorageChanged()`:`dianshan/src/popup/hook/use-scan.ts:146` - background 消息总入口:`chrome.runtime.onMessage.addListener(...)`:`dianshan/src/background/index.ts:22` - background 路由:`case "START_CRAWL"` 等:`dianshan/src/background/index.ts:34` - background 窗口/Tab 关闭监听:`windows.onRemoved`/`tabs.onRemoved`:`dianshan/src/background/index.ts:92` - 任务启动:`startCrawl()`:`dianshan/src/background/task/crawlTask.ts:14` - 任务执行器:`runCrawlSteps()`:`dianshan/src/background/task/crawlTask.ts:63` - 完成收尾:`finalizeCompletedTask()`:`dianshan/src/background/task/crawlTask.ts:312` - 取消任务:`cancelCrawl()`:`dianshan/src/background/task/crawlTask.ts:131` - 窗口关闭自动暂停:`pauseCrawlOnWindowRemoved()`:`dianshan/src/background/task/crawlTask.ts:158` - Tab 关闭自动暂停:`pauseCrawlOnTabRemoved()`:`dianshan/src/background/task/crawlTask.ts:188` - 继续/恢复:`resumeCrawl()`:`dianshan/src/background/task/crawlTask.ts:219` - 状态读写:`getCrawlTaskState()`/`setCrawlTaskState()`:`dianshan/src/background/task/taskState.ts:10` / `dianshan/src/background/task/taskState.ts:20` - 新开爬取窗口:`openSingleTabWindow()`:`dianshan/src/background/task/helper.ts:8` - 等待页面加载:`waitForTabLoaded()`:`dianshan/src/background/task/helper.ts:45` - 让 content 抓取:`scrapeStepInContent()`:`dianshan/src/background/task/helper.ts:90` - content 执行器入口:`setupPageRunner()`:`dianshan/src/content/pageRunner.ts:28` - content 抓取处理:`handlePageRunnerMessage()`:`dianshan/src/content/pageRunner.ts:38` - 爬取窗口悬浮 UI:`mountCrawlOverlay()`:`dianshan/src/content/crawlOverlay.ts:36` - 悬浮 UI 初始化拉取快照:`GET_CRAWL_STATE_FOR_TAB`:`dianshan/src/content/crawlOverlay.ts:67` - 悬浮 UI 实时状态推送:`CRAWL_STATE_UPDATE`:`dianshan/src/content/crawlOverlay.ts:54` --- ## 2. 核心数据结构:CrawlTaskState(状态机) 存储位置:`chrome.storage.local['crawlTaskState']` 关键字段(简化说明): - `id`:任务唯一 ID(`platformId-startedAt`) - `platformId/platformName`:平台信息 - `windowId/tabId`:当前爬取窗口与承载爬取的 tab(overlay 只在这个 tab 渲染) - `startedAt`:任务开始时间戳(用于 popup/overlay 计时) - `status`:`running | paused | completed | failed | canceled` - `pause`:当 `status='paused'` 时存在,包含 - `reason`:`reauth | shield | not_found | page_not_ready | window_closed` - `message`:给用户看的暂停提示 - `currentStepIndex`:当前执行到的步骤索引 - `steps[]`:每个步骤的进度条记录 - `uniqueKey/name` - `status`:`pending | running | success | failed` - `result/message`:抓取结果或失败原因 状态同步策略: - background 每次 `setCrawlTaskState()` / `updateCrawlTaskState()` 都会写入 storage - popup:监听 `chrome.storage.onChanged`,永远以 storage 为准渲染 UI - overlay:通过 `sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)` 实时更新(并可在初始化时请求快照) - 外部网页:`chrome.storage.onChanged` -> `externalBridge.broadcastCrawlStorageChange()` 广播结果/状态 --- ## 3. “点击爬取”后的完整时序(从 UI 到抓取) 以下用“触发方法 / 通信对象 / 状态变化”描述每一步。 ### 3.1 Popup 点击 “Scan now” 1) 触发方法 - popup:`use-scan.ts -> handleScan()`(`dianshan/src/popup/hook/use-scan.ts:46`) 2) 通信 - popup -> background:`sendBackgroundMessage({ action:'START_CRAWL', payload:{ platformId } })` 3) background 入口 - `background/index.ts` 的 `chrome.runtime.onMessage` 收到 `START_CRAWL`(`dianshan/src/background/index.ts:22` / `dianshan/src/background/index.ts:34`) - 调用:`crawlTask.startCrawl(platformId)`(`dianshan/src/background/task/crawlTask.ts:14`) 4) 状态变化(startCrawl) - `openSingleTabWindow(steps[0].url)`:创建爬取窗口(popup 类型)得到 `{windowId, tabId}`(`dianshan/src/background/task/helper.ts:8`) - 构建初始 `CrawlTaskState`: - `status='running'` - `currentStepIndex=0` - `steps[0].status='running'`,其余 `pending` - `setCrawlTaskState(nextState)` 写入 storage(`dianshan/src/background/task/taskState.ts:20`) - 这一步会触发: - popup 的 storage 监听更新 UI - background 同时 `sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)` 推送给 overlay(`dianshan/src/background/task/taskState.ts:20`) - externalBridge 通过 storage.onChanged 对外广播(如果网页有连 port) 5) 启动执行器 - background:创建 `AbortController`,并异步执行: - `runCrawlSteps(taskId, tabId, platform.steps, signal, startIndex=0)`(`dianshan/src/background/task/crawlTask.ts:63`) --- ## 4. “执行一个步骤”会发生什么(runCrawlSteps 的循环体) 每个 step 的处理流程如下(对每个 i 从 startIndex 到 steps.length-1): ### 4.1 进入新 step:先更新状态机 1) 触发方法 - background:`updateCrawlTaskState(taskId, updater)` 2) 状态变化 - `currentStepIndex = i` - `steps[i].status = 'running'`(其它步骤保持原样) 3) 通信影响 - 因为 `updateCrawlTaskState()` 内部会 `setCrawlTaskState()`: - storage 写入 -> popup UI 更新 - 同时 `sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)` -> overlay 更新 - storage.onChanged -> external bridge 广播(如果外部网页连着) ### 4.2 跳转页面并等待加载完成 1) 触发方法 - background:`chrome.tabs.update(tabId, { url: step.url, active: true })` - background:`waitForTabLoaded(tabId, signal)` 2) 通信对象 - background -> Chrome tabs API(无 content 参与) 3) 状态变化 - 不改变任务状态,只是准备让 content script 进入目标页面上下文 ### 4.3 让 content script 执行抓取(核心通信) 1) 触发方法 - background:`scrapeStepInContent(tabId, step, signal)` 2) 通信 - background -> content(爬取窗口 tab):`chrome.tabs.sendMessage(tabId, { action:'SCRAPE_STEP', payload:{ fields, checkSelector } })` 3) content 入口 - `content/pageRunner.ts` 的 `chrome.runtime.onMessage` 收到 `SCRAPE_STEP` - `detectPageInterrupt()`:先判断是否需要人工处理(登录/验证码/404/未就绪) - `waitForStableSelector(checkSelector, timeout)`:等待关键 DOM 稳定出现 - `processFields(fields, document.body)`:按配置抓取 DOM 数据 4) 返回值约定(PageRunnerResponse) - `ok: true, data: DomScrapeResult`:本步骤抓取成功 - `interrupt: CrawlPauseInfo`:需要人工处理(会进入 paused) - `ok: false, error`:未就绪/异常,background 会重试 ### 4.4 抓取成功:写入结果并标记 step success 1) 触发方法 - background:`updateCrawlTaskState(taskId, updater)` 2) 状态变化 - `steps[i].status = 'success'` - `steps[i].result = res.data` 3) 通信影响 - 同 4.1:storage + overlay 推送 + external bridge 广播 ### 4.5 抓取中断:进入 paused(需要用户处理) 中断来源: - content 检测到 `shield/reauth/not_found/page_not_ready` - 或者:爬取窗口被用户关掉(见第 6 章) 1) 触发方法(以 content 中断为例) - background:`updateCrawlTaskState(taskId, s => ({ ...s, status:'paused', pause: interrupt }))`(`dianshan/src/background/task/crawlTask.ts:93`) 2) 状态变化 - `status='paused'` - `pause={reason,message}` 3) 执行器行为 - background 在 `runCrawlSteps` 内部“死等恢复”: - 循环检查 `getCrawlTaskState()?.status === 'paused'` - 直到用户点击“继续”把状态改回 `running` 才继续 4) UI 行为 - popup / overlay 都会显示暂停提示 + “Continue” --- ## 5. “点击继续”会发生什么(RESUME_CRAWL) 触发来源: - popup 的 Continue - overlay 的 Continue ### 5.1 popup/overlay 发消息 - popup:`use-scan.ts -> handleResumeCrawl() -> sendBackgroundMessage({action:'RESUME_CRAWL'})`(`dianshan/src/popup/hook/use-scan.ts:83`) - overlay:`crawlOverlay.ts -> chrome.runtime.sendMessage({action:'RESUME_CRAWL'})`(`dianshan/src/content/crawlOverlay.ts:392`) ### 5.2 background 处理 resume 入口: - `background/index.ts` switch case `RESUME_CRAWL`(`dianshan/src/background/index.ts:66`) - 调用 `crawlTask.resumeCrawl()`(`dianshan/src/background/task/crawlTask.ts:219`) resumeCrawl 分两类: #### A) 登录/验证码等中断(窗口仍存在) 条件: - `state.status === 'paused'` - `state.pause.reason !== 'window_closed'` - `state.windowId && state.tabId` 还在 行为: - 仅把状态改回 `running`(清掉 pause) - 原来的 `runCrawlSteps()` 仍在“死等恢复”,会自动继续跑下一轮 #### B) 窗口被关闭导致中断(需要重开窗口) 条件: - `state.pause.reason === 'window_closed'` 或 `windowId/tabId` 不存在 行为: - 根据 `currentStepIndex` 找到“第一个未 success 的步骤”作为 `startIndex` - `openSingleTabWindow(url)` 重新打开爬取窗口(`dianshan/src/background/task/helper.ts:8`) - `setCrawlTaskState()` 写入新的 `windowId/tabId/status='running'/currentStepIndex=startIndex`(`dianshan/src/background/task/taskState.ts:20`) - 新建 `AbortController`,重新启动执行器: - `runCrawlSteps(taskId, newTabId, platform.steps, signal, startIndex)`(`dianshan/src/background/task/crawlTask.ts:63`) --- ## 6. “爬取过程中窗口被关掉”会发生什么(自动暂停) 触发点: - `background/index.ts` 监听: - `chrome.windows.onRemoved`(`dianshan/src/background/index.ts:92`) - `chrome.tabs.onRemoved`(兜底)(`dianshan/src/background/index.ts:97`) 处理逻辑: - 调用 `crawlTask.pauseCrawlOnWindowRemoved(windowId)`(`dianshan/src/background/task/crawlTask.ts:158`)或 `pauseCrawlOnTabRemoved(tabId)`(`dianshan/src/background/task/crawlTask.ts:188`) 行为: 1) 中止执行器 - 找到当前任务对应的 `AbortController` 并 `abort()` - 目的:避免后台继续对不存在的 tab 进行 `tabs.update/sendMessage` 导致刷屏错误 2) 状态改为 paused - `status='paused'` - `pause.reason='window_closed'` - `pause.message` 提示用户点击继续可恢复 - 清空 `windowId/tabId`(避免 UI 侧再尝试 focus 老窗口) 用户点击继续后: - 走第 5.2 的 B 分支:重开窗口 + 从未完成步骤继续 --- ## 7. “全部步骤完成”会发生什么(发送结果 + 清理 + 关窗) 触发点: - `runCrawlSteps()` 完成 for 循环后: 1) `updateCrawlTaskState(...status='completed')` 2) 调用 `finalizeCompletedTask(taskId)` finalizeCompletedTask 做三件事(顺序很重要): 1) 发送结果给页面 - background -> content(爬取 tab):`sendTabMessage(tabId,'CRAWL_COMPLETED',{ result: ... })` - 同时 storage 完成态会触发 `externalBridge` 对外广播 `DIANSHAN_CRAWL_DONE` 2) 清空本次任务记录 - `clearCrawlTaskState()`:移除 `chrome.storage.local['crawlTaskState']` - popup 会收到 storage 改变并自动把 UI 恢复到“未开始”状态 3) 关闭爬取窗口 - `chrome.windows.remove(windowId)` --- ## 8. Popup/Overlay 如何拿到状态(同步机制总结) ### 8.1 Popup:只看 storage(强一致) - 初次加载:发 `GET_CRAWL_STATE` - 后续更新:监听 `chrome.storage.onChanged`,只要 `crawlTaskState` 变化就刷新 UI ### 8.2 Overlay:推送为主,拉取为辅 - 推送:background 写入 state 时会 `sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)` - overlay 监听 `chrome.runtime.onMessage`,收到 `action==='CRAWL_STATE_UPDATE'` 立刻渲染 - 拉取:overlay 初始化时发 `GET_CRAWL_STATE_FOR_TAB` - background 只在 `sender.tab.id === state.tabId` 时返回 state,否则返回 null --- ## 9. 外部网页(官网/业务系统)如何接收结果(externalBridge) 两种方式: ### 9.1 onMessageExternal(短消息) - 网页发 `DIANSHAN_START_CRAWL / DIANSHAN_GET_CRAWL_STATE / ...` - background `externalBridge.handleExternalMessage()` 返回当前状态/平台列表等 ### 9.2 onConnectExternal(长连接 Port) - 网页建立 Port(name=`DIANSHAN_CRAWL`) - background 把当前 state 发一次 `DIANSHAN_CRAWL_STATE` - 后续只要 storage 变化,就广播: - `DIANSHAN_CRAWL_STATE`(进行中) - `DIANSHAN_CRAWL_DONE`(完成且带 result) - `DIANSHAN_CRAWL_CLEARED / FAILED / CANCELED`(清理/失败/取消) 结果结构: - `externalBridge.buildCrawlWebPayload(state)` 会给出: - `state`:完整状态机 - `result`:仅当 `state.status==='completed'` 时非空,为按 step.uniqueKey 聚合的结果对象 --- ## 10. 常见问题排查(建议) 1) overlay 不显示 / 不更新 - 先确认该 tab 是否为 `crawlTaskState.tabId` - overlay 初始化会请求 `GET_CRAWL_STATE_FOR_TAB`,只有爬取 tab 才返回 state - 确认 background 写入 state 时是否调用了 `setCrawlTaskState()`(它会推送 `CRAWL_STATE_UPDATE`) 2) 点击取消 popup “闪一下” - 通常是计时器回调解引用了空状态;popup 侧必须在 state 清空时 stop timer 3) 窗口被关后任务还在后台跑 - 应当由 `windows.onRemoved/tabs.onRemoved` 触发 `pauseCrawlOnWindowRemoved/TabRemoved` - 检查 background/index.ts 是否注册了监听 --- ## 11. 约定:消息 action 一览(当前) popup/content -> background(`dianshan/src/shared/message.ts`): - `START_CRAWL` - `GET_CRAWL_STATE` - `GET_CRAWL_STATE_FOR_TAB`(仅爬取 tab 返回 state) - `CANCEL_CRAWL` - `RESUME_CRAWL` - `DISMISS_CRAWL` - `CANCEL_AUTOCLOSE`(兼容旧 overlay 按钮,不建议新逻辑依赖) background -> content(`dianshan/src/shared/tab.ts`): - `CRAWL_STATE_UPDATE`(overlay/pagerunner 可用) - `CRAWL_COMPLETED`(完成时额外发送结果)