From d6dc8a0db4d54cade0e29b2e03aa9140c3c2ca5c Mon Sep 17 00:00:00 2001 From: zhu <1812073942@qq.com> Date: Thu, 7 May 2026 09:29:56 +0800 Subject: [PATCH] 1 --- src/background/service/crawlTask.ts | 3 +- we.md | 661 ++++++++++++++++++++++++++++ ww.md | 12 - 3 files changed, 662 insertions(+), 14 deletions(-) create mode 100644 we.md delete mode 100644 ww.md diff --git a/src/background/service/crawlTask.ts b/src/background/service/crawlTask.ts index cf6ed73..a008925 100644 --- a/src/background/service/crawlTask.ts +++ b/src/background/service/crawlTask.ts @@ -1,12 +1,11 @@ import { getPlatformById } from '@/config/platforms'; import type { CrawlPauseInfo, CrawlProgressStep, CrawlTaskState, PlatformConfig, PlatformStepConfig } from '@/types'; -import type { DomScrapeResult } from '../domScraper'; import type { CrawlStateResponse } from '../types'; import { getCrawlTaskState, setCrawlTaskState, updateCrawlTaskState } from './taskState'; interface PageRunnerResponse { ok: boolean; - data?: DomScrapeResult | null; + data?: any | null; interrupt?: CrawlPauseInfo; error?: string; } diff --git a/we.md b/we.md new file mode 100644 index 0000000..8ad6b35 --- /dev/null +++ b/we.md @@ -0,0 +1,661 @@ +# 店闪扩展执行链路说明 + +这份文档按当前项目代码整理,目的是方便顺着代码阅读整个爬取流程。 + +## 1. 扩展入口关系 + +扩展入口由 `manifest.config.ts` 配置: + +- `action.default_popup` 指向 `src/popup/index.html`,点击浏览器插件图标后打开 popup。 +- `background.service_worker` 指向 `src/background/index.ts`,负责接收消息、创建爬取窗口、执行爬取任务。 +- `content_scripts` 指向 `src/content/main.ts`,会注入到所有 `http/https` 页面,用于右下角进度浮窗和页面 DOM 抓取。 +- `permissions` 中的 `storage`、`tabs`、`windows` 支撑状态保存、页面跳转和窗口管理。 + +## 2. 点击 popup 后发生什么 + +### 2.1 popup 初始化 + +文件:`src/popup/App.vue` + +触发方法: + +- `onMounted` +- `getToken` +- `refreshCrawlState` +- `updateElapsedSeconds` + +执行过程: + +1. 用户点击扩展图标,Chrome 打开 `src/popup/index.html`。 +2. Vue 加载 `src/popup/App.vue`。 +3. `onMounted` 执行: + - 调用 `getToken()` 读取登录状态。 + - 调用 `refreshCrawlState()` 获取当前爬取任务。 + - 启动 `setInterval`,每秒刷新爬取状态和计时。 +4. `getToken()` 来自 `src/shared/auth.ts`: + - 优先从 `chrome.storage.local` 读取 `token`。 + - 非扩展环境下从 `localStorage` 读取。 + +### 2.2 点击“立即爬取” + +文件:`src/popup/App.vue` + +触发方法: + +- `handleScan` +- `sendBackgroundMessage` + +执行过程: + +1. 用户点击“立即爬取”按钮。 +2. `handleScan()` 检查当前是否选择了平台。 +3. 通过 `sendBackgroundMessage()` 发送消息给 background: + +```ts +{ + action: 'START_CRAWL', + payload: { platformId: selectedPlatform.value.id }, +} +``` + +4. `sendBackgroundMessage()` 实际调用: + +```ts +chrome.runtime.sendMessage(message) +``` + +5. popup 收到 background 返回的 `CrawlTaskState` 后,更新本地的 `crawlState`,页面开始显示进度。 + +## 3. background 如何接住 popup 消息 + +### 3.1 background 消息入口 + +文件:`src/background/index.ts` + +触发方法: + +- `chrome.runtime.onMessage.addListener` +- `handleBackgroundMessage` + +执行过程: + +1. background service worker 启动后,注册 `chrome.runtime.onMessage`。 +2. popup 发来的 `START_CRAWL` 会进入 `handleBackgroundMessage()`。 +3. `handleBackgroundMessage()` 调用: + +```ts +handleBackgroundCommand(message) +``` + +4. 如果执行成功,调用 `sendResponse(result)` 把结果回给 popup。 +5. 如果执行失败,统一返回: + +```ts +{ ok: false, error: messageText } +``` + +### 3.2 background 指令分发 + +文件:`src/background/service.ts` + +作用: + +```ts +export { + handleBackgroundCommand, + handleInstalled, + handleStartup, + handleWindowRemoved, +} from './service/lifecycle'; +``` + +这个文件现在只是 re-export,真正逻辑在 `src/background/service/lifecycle.ts`。 + +文件:`src/background/service/lifecycle.ts` + +触发方法: + +- `handleBackgroundCommand` + +分发关系: + +- `START_CRAWL` -> `startCrawl` +- `GET_CRAWL_STATE` -> `getCrawlTaskState` +- `CANCEL_CRAWL` -> `cancelCrawl` +- `RESUME_CRAWL` -> `resumeCrawl` + +这些消息类型定义在 `src/background/types.ts`。 + +## 4. 创建爬取任务和新窗口 + +文件:`src/background/service/crawlTask.ts` + +触发方法: + +- `startCrawl` +- `createCrawlWindow` +- `runCrawlSteps` + +执行过程: + +1. `startCrawl(platformId)` 先调用 `getPlatformById(platformId)`。 +2. `getPlatformById` 来自 `src/config/platforms.ts`,用于找到当前平台配置。 +3. 读取平台配置中的第一个 step: + +```ts +const firstStep = platform.steps[0]; +``` + +4. 创建初始任务状态 `CrawlTaskState`: + - `id` + - `platformId` + - `platformName` + - `startedAt` + - `status: 'running'` + - `currentStepIndex: 0` + - `steps` +5. 调用 `setCrawlTaskState(nextState)` 写入 `chrome.storage.local`。 +6. 调用 `createCrawlWindow(firstStep.url)` 打开新的普通浏览器窗口。 +7. 窗口创建成功后,把 `windowId` 写回任务状态。 +8. 调用: + +```ts +void runCrawlSteps(platform, stateWithWindow); +``` + +这里使用 `void`,表示后台任务异步继续跑;`startCrawl` 会先把初始状态返回给 popup。 + +## 5. 爬取状态保存在哪里 + +文件:`src/background/service/taskState.ts` + +核心方法: + +- `getCrawlTaskState` +- `setCrawlTaskState` +- `updateCrawlTaskState` + +保存位置: + +```ts +chrome.storage.local +``` + +保存 key: + +```ts +crawlTaskState +``` + +当前项目没有把爬取结果单独保存到数据库、文件或独立 result key。每一步的结果直接保存在: + +```ts +CrawlTaskState.steps[index].result +``` + +也就是说,popup 和 content 看到的进度、暂停信息、最终结果,都来自 `chrome.storage.local` 中的 `crawlTaskState`。 + +## 6. background 如何逐步爬取页面 + +文件:`src/background/service/crawlTask.ts` + +核心方法: + +- `runCrawlSteps` +- `getWindowActiveTabId` +- `waitForTabLoaded` +- `scrapeStepInContent` +- `sendPageRunnerMessage` +- `pauseForInterrupt` +- `waitUntilResumed` + +执行过程: + +1. `runCrawlSteps(platform, initialState)` 按 `platform.steps` 顺序循环。 +2. 每进入一个 step,会调用 `updateCrawlTaskState`: + - 更新 `currentStepIndex`。 + - 把当前 step 标记为 `running`。 + - 清空当前 step 的旧 message。 +3. 调用 `getWindowActiveTabId(windowId)` 找到爬取窗口里的当前 tab。 +4. 调用: + +```ts +chrome.tabs.update(tabId, { url: step.url, active: true }) +``` + +跳转到当前 step 配置的页面地址。 + +5. 调用 `waitForTabLoaded(tabId)` 等待 Chrome 的 tab 状态变成 `complete`。 +6. 调用 `scrapeStepInContent(tabId, step)`,让目标页面里的 content script 开始检查页面和抓取 DOM。 + +## 7. background 怎么通知目标网页抓取 + +文件:`src/background/service/crawlTask.ts` + +触发方法: + +- `scrapeStepInContent` +- `sendPageRunnerMessage` + +发送消息: + +```ts +{ + action: 'SCRAPE_STEP', + payload: { + fields: step.fields, + checkSelector: step.checkSelector, + }, +} +``` + +实际调用: + +```ts +chrome.tabs.sendMessage(tabId, message) +``` + +这里不是 popup 发给 background,而是 background 发给目标网页 tab 里的 content script。 + +`scrapeStepInContent` 还有一个容错逻辑:如果 content script 还没注入完成,出现类似 `Could not establish connection. Receiving end does not exist.` 的错误,会在 20 秒内每 500ms 重试一次。 + +## 8. content script 如何接住抓取消息 + +### 8.1 content script 挂载 + +文件:`src/content/main.ts` + +触发方法: + +- `mountApp` +- `setupPageRunner` + +执行过程: + +1. 目标页面加载时,Chrome 根据 `manifest.config.ts` 注入 `src/content/main.ts`。 +2. `main.ts` 等待 DOM 可用后执行 `mountApp()`。 +3. `mountApp()` 创建 `#dianshan-crx-root` 容器。 +4. 挂载 `src/content/App.vue`,用于显示右下角爬取计时按钮和进度面板。 +5. 调用 `setupPageRunner()` 注册页面消息监听器。 + +### 8.2 页面执行器接收 SCRAPE_STEP + +文件:`src/content/pageRunner.ts` + +触发方法: + +- `setupPageRunner` +- `handlePageRunnerMessage` +- `detectPageInterrupt` +- `waitForStableSelector` +- `processFields` + +执行过程: + +1. `setupPageRunner()` 注册: + +```ts +chrome.runtime.onMessage.addListener(...) +``` + +2. background 发送 `SCRAPE_STEP` 后,进入 `handlePageRunnerMessage()`。 +3. 先调用 `detectPageInterrupt()` 判断是否遇到: + - 登录页:`reauth` + - 验证码或风控:`shield` + - 页面不存在:`not_found` +4. 如果检测到中断,返回: + +```ts +{ ok: false, interrupt } +``` + +5. 如果没有中断,调用 `waitForStableSelector(checkSelector, 18000)`。 +6. `checkSelector` 来自 `src/config/platforms.ts` 的 step 配置,用于判断页面关键 DOM 是否出现并可见。 +7. 如果 18 秒内关键 DOM 仍未稳定出现,返回 `page_not_ready` 中断。 +8. 如果页面可用,调用: + +```ts +processFields(message.payload.fields, document.body) +``` + +开始真正的 DOM 字段采集。 + +## 9. DOM 数据怎么提取 + +文件:`src/background/domScraper.ts` + +虽然文件在 `background` 目录下,但它被 `src/content/pageRunner.ts` import 后,实际是在目标网页的 content script 环境里执行 DOM 查询。 + +核心方法: + +- `processFields` +- `processList` +- `processTable` +- `autoClick` +- `extractValue` +- `waitForElement` + +执行逻辑: + +1. `processFields(columns, rootDom)` 遍历当前 step 的 `fields`。 +2. 每个字段先执行 `autoClick(item, rootDom)`: + - 如果字段配置了 `condition.list`,就按顺序点击对应选择器。 + - 每次点击后等待 `condition.time`。 +3. 再根据 `item.className` 查询元素。 +4. 普通字段: + - 没有 `keys` 时,调用 `extractValue(element, item)`。 + - 有 `keys` 时,递归调用 `processFields(item.keys, element)`。 +5. 列表字段: + - `type === 1` 时进入 `processList`。 + - 按 `config.className` 找到列表项。 + - 对每个列表项递归执行 `processFields(config.keys, element)`。 + - 如果配置了 `pagination`,会点击下一页继续采集。 +6. 表格字段: + - `type === 2` 时进入 `processTable`。 + - 按 `tableParts` 找到不同 table 片段。 + - 以第一个 part 的行数为准,按行拼接不同 part 的字段。 + - 如果配置了 `pagination`,会点击下一页继续采集。 +7. `extractValue` 的取值规则: + - 配置了 `attr` 就取指定属性。 + - `IMG` 默认取 `src`。 + - `A` 默认取 `href`,相对路径会拼上当前 origin。 + - 其他元素默认取 `textContent`。 + +## 10. 采集结果怎么回传和保存 + +数据流向: + +1. `processFields` 返回当前 step 的 DOM 采集结果。 +2. `src/content/pageRunner.ts` 返回: + +```ts +{ ok: true, data } +``` + +3. `src/background/service/crawlTask.ts` 的 `scrapeStepInContent` 接到 response。 +4. `runCrawlSteps` 调用 `updateCrawlTaskState`: + +```ts +steps[index].result = response.data +steps[index].status = 'success' +``` + +5. 最新任务状态写回 `chrome.storage.local` 的 `crawlTaskState`。 +6. popup 和 content 浮窗每秒发送 `GET_CRAWL_STATE`,读取到最新结果并展示。 + +所以当前项目的数据传递是: + +```text +目标网页 DOM + -> content/pageRunner.ts + -> background/crawlTask.ts + -> chrome.storage.local:crawlTaskState + -> popup/App.vue 和 content/App.vue 轮询展示 +``` + +## 11. popup 进度如何刷新 + +文件:`src/popup/App.vue` + +触发方法: + +- `refreshCrawlState` +- `sendBackgroundMessage` +- `updateElapsedSeconds` + +执行过程: + +1. popup 打开后每秒调用一次 `refreshCrawlState()`。 +2. `refreshCrawlState()` 发送: + +```ts +{ action: 'GET_CRAWL_STATE' } +``` + +3. background 的 `handleBackgroundCommand` 收到后调用 `getCrawlTaskState()`。 +4. popup 拿到 `CrawlTaskState` 后: + - 展示平台名。 + - 展示运行时间。 + - 展示每个 step 的状态。 + - 如果 step 有 `result`,用 `JSON.stringify(step.result, null, 2)` 打印出来。 + +## 12. 目标网页右下角按钮如何刷新 + +文件:`src/content/App.vue` + +触发方法: + +- `onMounted` +- `refreshCrawlState` +- `updateElapsedSeconds` +- `handleResumeCrawl` + +执行过程: + +1. content script 挂载后,`src/content/App.vue` 每秒调用 `refreshCrawlState()`。 +2. 它同样发送: + +```ts +{ action: 'GET_CRAWL_STATE' } +``` + +3. 如果任务状态是 `running` 或 `paused`,显示右下角计时按钮。 +4. 点击按钮会展开进度面板。 +5. 如果任务是 `paused`,面板里显示暂停原因,并提供“我已处理,继续”按钮。 +6. 点击继续时,调用 `handleResumeCrawl()`,发送: + +```ts +{ action: 'RESUME_CRAWL' } +``` + +## 13. 暂停和继续流程 + +触发原因主要来自 `src/content/pageRunner.ts`: + +- `isLoginPage()` 检测到登录页。 +- `isShieldPage()` 检测到验证码或风控。 +- `isNotFoundPage()` 检测到页面不存在。 +- `waitForStableSelector()` 超时,认为页面关键内容没准备好。 + +流程: + +1. content 返回: + +```ts +{ ok: false, interrupt } +``` + +2. background 的 `runCrawlSteps` 检测到 `response.interrupt`。 +3. 调用 `pauseForInterrupt(taskId, stepIndex, interrupt)`。 +4. `pauseForInterrupt` 把状态写成: + - `status: 'paused'` + - `pause: interrupt` + - 当前 step 保持 `running` + - 当前 step 的 `message` 设置为中断提示 +5. popup 和 content 每秒轮询到 paused 状态后展示提示。 +6. 用户处理完登录或验证码后,点击继续。 +7. popup 或 content 发送 `RESUME_CRAWL`。 +8. background 调用 `resumeCrawl()`: + - `status` 改回 `running` + - 清空 `pause` + - 当前 step 的 `message` 清空 +9. `runCrawlSteps` 中的 `waitUntilResumed()` 发现状态恢复为 `running`,重新执行当前 step。 + +## 14. 取消和关闭窗口流程 + +### 14.1 用户点击取消 + +文件: + +- `src/popup/App.vue` +- `src/background/service/lifecycle.ts` +- `src/background/service/crawlTask.ts` + +方法: + +- `handleCancelCrawl` +- `handleBackgroundCommand` +- `cancelCrawl` + +流程: + +1. popup 发送: + +```ts +{ action: 'CANCEL_CRAWL' } +``` + +2. background 分发到 `cancelCrawl()`。 +3. `cancelCrawl()` 把当前任务状态改成 `canceled`。 +4. 当前 step 被标记为 `failed`,message 为 `用户已取消`。 +5. 如果存在 `windowId`,调用 `chrome.windows.remove(windowId)` 关闭爬取窗口。 + +### 14.2 用户直接关闭爬取窗口 + +文件: + +- `src/background/index.ts` +- `src/background/service/lifecycle.ts` +- `src/background/service/crawlTask.ts` + +方法: + +- `chrome.windows.onRemoved.addListener` +- `handleWindowRemoved` +- `cancelCrawlWhenWindowRemoved` + +流程: + +1. Chrome 触发 `windows.onRemoved`。 +2. `handleWindowRemoved(windowId)` 被调用。 +3. `cancelCrawlWhenWindowRemoved(windowId)` 检查关闭的是否是当前爬取窗口。 +4. 如果匹配且任务还在 running,就把任务改成 `canceled`。 +5. 当前 step 标记为 `failed`,message 为 `爬取窗口已关闭`。 + +## 15. 平台配置如何驱动爬取 + +文件:`src/config/platforms.ts` + +核心导出: + +- `PLATFORM_CONFIGS` +- `getPlatformById` + +平台配置结构来自 `src/types/platform.ts`: + +- `PlatformConfig` +- `PlatformStepConfig` +- `PlatformFieldConfig` +- `PlatformPaginationConfig` +- `PlatformTablePartConfig` +- `PlatformClickCondition` + +关键字段: + +- `steps`:平台要按顺序爬取的页面。 +- `step.url`:当前步骤要打开的页面地址。 +- `step.checkSelector`:判断页面是否可抓取的关键元素。 +- `step.fields`:当前页面要采集的字段。 +- `field.className`:字段选择器。 +- `field.keys`:子字段,支持递归。 +- `field.type`:字段类型,默认普通字段,`1` 是列表,`2` 是表格。 +- `field.condition`:采集字段前要自动点击的元素。 +- `field.pagination`:列表或表格翻页配置。 +- `field.tableParts`:表格分段配置,用来拼接多个 table 片段。 + +## 16. 类型结构在哪里看 + +主要类型文件: + +- `src/background/types.ts`:background 接收的消息类型和统一响应类型。 +- `src/types/crawl.ts`:爬取任务状态、步骤状态、暂停原因。 +- `src/types/platform.ts`:平台配置、字段配置、分页配置、表格配置。 +- `src/types/index.ts`:统一导出类型。 + +最关键的运行时状态是 `CrawlTaskState`: + +```ts +interface CrawlTaskState { + id: string; + platformId: string; + platformName: string; + windowId?: number; + startedAt: number; + status: 'running' | 'paused' | 'completed' | 'failed' | 'canceled'; + pause?: CrawlPauseInfo; + currentStepIndex: number; + steps: CrawlProgressStep[]; +} +``` + +每个 step 的结果保存在: + +```ts +interface CrawlProgressStep { + name: string; + uniqueKey: string; + status: 'pending' | 'running' | 'success' | 'failed'; + message?: string; + result?: unknown; +} +``` + +## 17. 整体数据流图 + +```text +用户点击插件图标 + -> src/popup/App.vue:onMounted + -> 读取 token 和 crawlTaskState + +用户点击立即爬取 + -> src/popup/App.vue:handleScan + -> chrome.runtime.sendMessage({ action: 'START_CRAWL' }) + +background 接收消息 + -> src/background/index.ts:handleBackgroundMessage + -> src/background/service/lifecycle.ts:handleBackgroundCommand + -> src/background/service/crawlTask.ts:startCrawl + +创建任务和窗口 + -> src/background/service/taskState.ts:setCrawlTaskState + -> src/background/service/crawlTask.ts:createCrawlWindow + -> src/background/service/crawlTask.ts:runCrawlSteps + +逐个页面跳转 + -> chrome.tabs.update(tabId, { url: step.url }) + -> src/background/service/crawlTask.ts:waitForTabLoaded + -> src/background/service/crawlTask.ts:scrapeStepInContent + +目标页面执行抓取 + -> chrome.tabs.sendMessage({ action: 'SCRAPE_STEP' }) + -> src/content/pageRunner.ts:handlePageRunnerMessage + -> src/content/pageRunner.ts:waitForStableSelector + -> src/background/domScraper.ts:processFields + +结果回到 background + -> src/background/service/crawlTask.ts:runCrawlSteps + -> src/background/service/taskState.ts:updateCrawlTaskState + -> 写入 chrome.storage.local:crawlTaskState + +多端展示 + -> src/popup/App.vue 每秒 GET_CRAWL_STATE + -> src/content/App.vue 每秒 GET_CRAWL_STATE + -> popup 展示完整进度和结果 + -> 目标网页右下角展示计时按钮和进度面板 +``` + +## 18. 推荐阅读代码顺序 + +如果要按最顺的方式读代码,可以这样看: + +1. `manifest.config.ts`:先看扩展入口。 +2. `src/popup/App.vue`:看用户点按钮时发什么消息。 +3. `src/background/index.ts`:看 background 怎么接消息。 +4. `src/background/service/lifecycle.ts`:看消息怎么分发。 +5. `src/background/service/crawlTask.ts`:看任务创建、窗口打开、页面跳转、暂停恢复。 +6. `src/background/service/taskState.ts`:看状态怎么保存。 +7. `src/content/main.ts`:看 content script 怎么挂载。 +8. `src/content/pageRunner.ts`:看目标页面怎么接收 background 抓取指令。 +9. `src/background/domScraper.ts`:看 DOM 字段怎么递归提取。 +10. `src/config/platforms.ts`:结合抓取逻辑看配置如何控制页面和字段。 +11. `src/types/crawl.ts`、`src/types/platform.ts`:最后看数据结构。 diff --git a/ww.md b/ww.md deleted file mode 100644 index 6c04f29..0000000 --- a/ww.md +++ /dev/null @@ -1,12 +0,0 @@ -# 原型 -请阅读storeai-extension-v0.1.0这个目录的代码后,请总结学习里面的开始爬取的执行流程、代码后,然后你在重构我这个项目 - -# 注意要求 -- 数据爬取逻辑,使用我写的方法,通过domScraper中的processFields方法直接调用爬取数据 -- 进度ui可以以原型中的为准 -- 撞盾和登录效验,也以原型里的代码逻辑为准 - - - - -