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

18 KiB
Raw Blame History

店闪扩展执行链路说明

这份文档按当前项目代码整理,目的是方便顺着代码阅读整个爬取流程。

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 中的 storagetabswindows 支撑状态保存、页面跳转和窗口管理。

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
{
  action: 'START_CRAWL',
  payload: { platformId: selectedPlatform.value.id },
}
  1. sendBackgroundMessage() 实际调用:
chrome.runtime.sendMessage(message)
  1. 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() 调用:
handleBackgroundCommand(message)
  1. 如果执行成功,调用 sendResponse(result) 把结果回给 popup。
  2. 如果执行失败,统一返回:
{ ok: false, error: messageText }

3.2 background 指令分发

文件:src/background/service.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
const firstStep = platform.steps[0];
  1. 创建初始任务状态 CrawlTaskState
    • id
    • platformId
    • platformName
    • startedAt
    • status: 'running'
    • currentStepIndex: 0
    • steps
  2. 调用 setCrawlTaskState(nextState) 写入 chrome.storage.local
  3. 调用 createCrawlWindow(firstStep.url) 打开新的普通浏览器窗口。
  4. 窗口创建成功后,把 windowId 写回任务状态。
  5. 调用:
void runCrawlSteps(platform, stateWithWindow);

这里使用 void,表示后台任务异步继续跑;startCrawl 会先把初始状态返回给 popup。

5. 爬取状态保存在哪里

文件:src/background/service/taskState.ts

核心方法:

  • getCrawlTaskState
  • setCrawlTaskState
  • updateCrawlTaskState

保存位置:

chrome.storage.local

保存 key

crawlTaskState

当前项目没有把爬取结果单独保存到数据库、文件或独立 result key。每一步的结果直接保存在

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. 调用:
chrome.tabs.update(tabId, { url: step.url, active: true })

跳转到当前 step 配置的页面地址。

  1. 调用 waitForTabLoaded(tabId) 等待 Chrome 的 tab 状态变成 complete
  2. 调用 scrapeStepInContent(tabId, step),让目标页面里的 content script 开始检查页面和抓取 DOM。

7. background 怎么通知目标网页抓取

文件:src/background/service/crawlTask.ts

触发方法:

  • scrapeStepInContent
  • sendPageRunnerMessage

发送消息:

{
  action: 'SCRAPE_STEP',
  payload: {
    fields: step.fields,
    checkSelector: step.checkSelector,
  },
}

实际调用:

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() 注册:
chrome.runtime.onMessage.addListener(...)
  1. background 发送 SCRAPE_STEP 后,进入 handlePageRunnerMessage()
  2. 先调用 detectPageInterrupt() 判断是否遇到:
    • 登录页:reauth
    • 验证码或风控:shield
    • 页面不存在:not_found
  3. 如果检测到中断,返回:
{ ok: false, interrupt }
  1. 如果没有中断,调用 waitForStableSelector(checkSelector, 18000)
  2. checkSelector 来自 src/config/platforms.ts 的 step 配置,用于判断页面关键 DOM 是否出现并可见。
  3. 如果 18 秒内关键 DOM 仍未稳定出现,返回 page_not_ready 中断。
  4. 如果页面可用,调用:
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 返回:
{ ok: true, data }
  1. src/background/service/crawlTask.tsscrapeStepInContent 接到 response。
  2. runCrawlSteps 调用 updateCrawlTaskState
steps[index].result = response.data
steps[index].status = 'success'
  1. 最新任务状态写回 chrome.storage.localcrawlTaskState
  2. popup 和 content 浮窗每秒发送 GET_CRAWL_STATE,读取到最新结果并展示。

所以当前项目的数据传递是:

目标网页 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() 发送:
{ action: 'GET_CRAWL_STATE' }
  1. background 的 handleBackgroundCommand 收到后调用 getCrawlTaskState()
  2. 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. 它同样发送:
{ action: 'GET_CRAWL_STATE' }
  1. 如果任务状态是 runningpaused,显示右下角计时按钮。
  2. 点击按钮会展开进度面板。
  3. 如果任务是 paused,面板里显示暂停原因,并提供“我已处理,继续”按钮。
  4. 点击继续时,调用 handleResumeCrawl(),发送:
{ action: 'RESUME_CRAWL' }

13. 暂停和继续流程

触发原因主要来自 src/content/pageRunner.ts

  • isLoginPage() 检测到登录页。
  • isShieldPage() 检测到验证码或风控。
  • isNotFoundPage() 检测到页面不存在。
  • waitForStableSelector() 超时,认为页面关键内容没准备好。

流程:

  1. content 返回:
{ ok: false, interrupt }
  1. background 的 runCrawlSteps 检测到 response.interrupt
  2. 调用 pauseForInterrupt(taskId, stepIndex, interrupt)
  3. pauseForInterrupt 把状态写成:
    • status: 'paused'
    • pause: interrupt
    • 当前 step 保持 running
    • 当前 step 的 message 设置为中断提示
  4. popup 和 content 每秒轮询到 paused 状态后展示提示。
  5. 用户处理完登录或验证码后,点击继续。
  6. popup 或 content 发送 RESUME_CRAWL
  7. background 调用 resumeCrawl()
    • status 改回 running
    • 清空 pause
    • 当前 step 的 message 清空
  8. 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 发送:
{ action: 'CANCEL_CRAWL' }
  1. background 分发到 cancelCrawl()
  2. cancelCrawl() 把当前任务状态改成 canceled
  3. 当前 step 被标记为 failedmessage 为 用户已取消
  4. 如果存在 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 标记为 failedmessage 为 爬取窗口已关闭

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.tsbackground 接收的消息类型和统一响应类型。
  • src/types/crawl.ts:爬取任务状态、步骤状态、暂停原因。
  • src/types/platform.ts:平台配置、字段配置、分页配置、表格配置。
  • src/types/index.ts:统一导出类型。

最关键的运行时状态是 CrawlTaskState

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 的结果保存在:

interface CrawlProgressStep {
  name: string;
  uniqueKey: string;
  status: 'pending' | 'running' | 'success' | 'failed';
  message?: string;
  result?: unknown;
}

17. 整体数据流图

用户点击插件图标
  -> 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.tssrc/types/platform.ts:最后看数据结构。