Files
store_ai_extension/we.md
2026-05-07 09:29:56 +08:00

662 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 店闪扩展执行链路说明
这份文档按当前项目代码整理,目的是方便顺着代码阅读整个爬取流程。
## 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`:最后看数据结构。