This commit is contained in:
zhu
2026-05-07 09:29:56 +08:00
parent a22f1a42e4
commit d6dc8a0db4
3 changed files with 662 additions and 14 deletions

View File

@@ -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;
}

661
we.md Normal file
View File

@@ -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`:最后看数据结构。

12
ww.md
View File

@@ -1,12 +0,0 @@
# 原型
请阅读storeai-extension-v0.1.0这个目录的代码后,请总结学习里面的开始爬取的执行流程、代码后,然后你在重构我这个项目
# 注意要求
- 数据爬取逻辑使用我写的方法通过domScraper中的processFields方法直接调用爬取数据
- 进度ui可以以原型中的为准
- 撞盾和登录效验,也以原型里的代码逻辑为准