1
This commit is contained in:
493
s.md
493
s.md
@@ -1,188 +1,389 @@
|
|||||||
# 网站接入店闪扩展说明
|
# dianshan 插件爬取任务开发文档(逻辑/通信/状态)
|
||||||
|
|
||||||
这个扩展已经提供网站侧调用接口。网站点击“开始”时,可以让扩展执行和 popup 手动点击“立即爬取”一样的流程:打开新浏览器窗口、进入平台后台、抓取数据。抓取完成后,扩展会通过长连接把结果推回网站。
|
> 目的:把“点击爬取后发生了什么、会触发哪些方法、给谁通信、改变哪些状态”说清楚,方便后续二开与排查。
|
||||||
|
>
|
||||||
|
> 范围:`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. 先配置允许连接的网站域名
|
---
|
||||||
|
|
||||||
扩展的 `manifest.config.ts` 里有:
|
## 1. 关键文件索引(按职责)
|
||||||
|
|
||||||
```ts
|
### 1.1 Popup(扩展弹窗 UI)
|
||||||
externally_connectable: {
|
- `dianshan/src/popup/App.vue`
|
||||||
matches: [
|
- UI 展示:平台选择、开始/取消/继续按钮、步骤列表、底部语言切换/版本号
|
||||||
"http://localhost:3000/*",
|
- `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 / ...` 等事件
|
||||||
|
|
||||||
```ts
|
### 1.3 Content Script(爬取窗口页内逻辑)
|
||||||
externally_connectable: {
|
- `dianshan/src/content/pageRunner.ts`
|
||||||
matches: [
|
- 接收 background 指令:
|
||||||
"http://localhost:3000/*",
|
- `CHECK_INTERRUPT`:判断登录/验证码/404/未就绪等“需要人工处理”的中断
|
||||||
"https://your-site.com/*",
|
- `SCRAPE_STEP`:等待关键 selector 稳定后执行 DOM 抓取
|
||||||
]
|
- `dianshan/src/content/crawlOverlay.ts`
|
||||||
}
|
- 爬取窗口左下角悬浮 UI(圆形计时菜单)
|
||||||
```
|
- 点击圆形菜单展开后展示“与 popup 类似”的步骤进度/暂停提示/继续/取消
|
||||||
|
- 接收 background 推送的 `CRAWL_STATE_UPDATE` 状态
|
||||||
|
- 初始化时会拉取一次 `GET_CRAWL_STATE_FOR_TAB` 快照(只在爬取 tab 返回状态)
|
||||||
|
|
||||||
改完扩展后需要重新 `pnpm run build`,并在 Chrome 扩展管理页重新加载扩展。
|
### 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` 等任务数据结构
|
||||||
|
|
||||||
## 2. 网站侧需要知道扩展 ID
|
### 1.5 关键方法定位(按真实代码)
|
||||||
|
> 下面列的都是当前项目里“确实存在”的方法/入口,后面章节会反复引用;每条都附带文件地址+行号,方便你 Ctrl+P 直达。
|
||||||
|
|
||||||
Chrome 扩展管理页打开“开发者模式”,复制这个扩展的 ID:
|
- 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`
|
||||||
|
|
||||||
```ts
|
- background 消息总入口:`chrome.runtime.onMessage.addListener(...)`:`dianshan/src/background/index.ts:22`
|
||||||
const EXTENSION_ID = "这里换成你的扩展ID";
|
- background 路由:`case "START_CRAWL"` 等:`dianshan/src/background/index.ts:34`
|
||||||
```
|
- background 窗口/Tab 关闭监听:`windows.onRemoved`/`tabs.onRemoved`:`dianshan/src/background/index.ts:92`
|
||||||
|
|
||||||
开发环境如果每次扩展 ID 变化,建议给扩展配置固定 key,或者每次复制新的 ID 到网站项目配置里。
|
- 任务启动:`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`
|
||||||
|
|
||||||
## 3. 推荐的网站侧接入代码
|
- 状态读写:`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`
|
||||||
|
|
||||||
```ts
|
- content 执行器入口:`setupPageRunner()`:`dianshan/src/content/pageRunner.ts:28`
|
||||||
const EXTENSION_ID = "这里换成你的扩展ID";
|
- content 抓取处理:`handlePageRunnerMessage()`:`dianshan/src/content/pageRunner.ts:38`
|
||||||
|
|
||||||
type DianshanMessage = {
|
- 爬取窗口悬浮 UI:`mountCrawlOverlay()`:`dianshan/src/content/crawlOverlay.ts:36`
|
||||||
ok: boolean;
|
- 悬浮 UI 初始化拉取快照:`GET_CRAWL_STATE_FOR_TAB`:`dianshan/src/content/crawlOverlay.ts:67`
|
||||||
type?: string;
|
- 悬浮 UI 实时状态推送:`CRAWL_STATE_UPDATE`:`dianshan/src/content/crawlOverlay.ts:54`
|
||||||
data?: {
|
|
||||||
state: any | null;
|
|
||||||
result: Record<string, unknown> | null;
|
|
||||||
};
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let port: chrome.runtime.Port | null = null;
|
---
|
||||||
|
|
||||||
export function connectDianshanExtension() {
|
## 2. 核心数据结构:CrawlTaskState(状态机)
|
||||||
port = chrome.runtime.connect(EXTENSION_ID, { name: "DIANSHAN_CRAWL" });
|
|
||||||
|
|
||||||
port.onMessage.addListener((message: DianshanMessage) => {
|
存储位置:`chrome.storage.local['crawlTaskState']`
|
||||||
console.log("[dianshan]", message);
|
|
||||||
|
|
||||||
if (message.type === "DIANSHAN_CRAWL_STATE") {
|
关键字段(简化说明):
|
||||||
// 可选:更新网站上的进度 UI
|
- `id`:任务唯一 ID(`platformId-startedAt`)
|
||||||
return;
|
- `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`:抓取结果或失败原因
|
||||||
|
|
||||||
if (message.type === "DIANSHAN_CRAWL_DONE") {
|
状态同步策略:
|
||||||
// 抓取完成,最终数据在 message.data.result
|
- background 每次 `setCrawlTaskState()` / `updateCrawlTaskState()` 都会写入 storage
|
||||||
console.log("抓取结果", message.data?.result);
|
- popup:监听 `chrome.storage.onChanged`,永远以 storage 为准渲染 UI
|
||||||
return;
|
- overlay:通过 `sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)` 实时更新(并可在初始化时请求快照)
|
||||||
}
|
- 外部网页:`chrome.storage.onChanged` -> `externalBridge.broadcastCrawlStorageChange()` 广播结果/状态
|
||||||
|
|
||||||
if (message.type === "DIANSHAN_CRAWL_FAILED") {
|
---
|
||||||
// 抓取失败,可展示 message.data.state.steps 里的失败原因
|
|
||||||
console.error("抓取失败", message.data?.state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === "DIANSHAN_CRAWL_CANCELED" || message.type === "DIANSHAN_CRAWL_CLEARED") {
|
## 3. “点击爬取”后的完整时序(从 UI 到抓取)
|
||||||
// 用户取消或任务被清空
|
|
||||||
console.log("抓取已取消");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
port.onDisconnect.addListener(() => {
|
以下用“触发方法 / 通信对象 / 状态变化”描述每一步。
|
||||||
port = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 网站点击“开始抓取”
|
### 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 } })`
|
||||||
|
|
||||||
```ts
|
3) background 入口
|
||||||
export async function startDianshanCrawl(platformId = "Shopee") {
|
- `background/index.ts` 的 `chrome.runtime.onMessage` 收到 `START_CRAWL`(`dianshan/src/background/index.ts:22` / `dianshan/src/background/index.ts:34`)
|
||||||
const response = await chrome.runtime.sendMessage(EXTENSION_ID, {
|
- 调用:`crawlTask.startCrawl(platformId)`(`dianshan/src/background/task/crawlTask.ts:14`)
|
||||||
type: "DIANSHAN_START_CRAWL",
|
|
||||||
payload: { platformId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response?.ok) {
|
4) 状态变化(startCrawl)
|
||||||
throw new Error(response?.error ?? "启动抓取失败");
|
- `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)
|
||||||
|
|
||||||
return response.data;
|
5) 启动执行器
|
||||||
}
|
- background:创建 `AbortController`,并异步执行:
|
||||||
```
|
- `runCrawlSteps(taskId, tabId, platform.steps, signal, startIndex=0)`(`dianshan/src/background/task/crawlTask.ts:63`)
|
||||||
|
|
||||||
效果等同于用户打开 popup 后手动点击“立即爬取”。如果当前已经有 running/paused 的任务,扩展会直接返回当前任务,不会重复打开多个抓取窗口。
|
---
|
||||||
|
|
||||||
## 5. 查询当前状态
|
## 4. “执行一个步骤”会发生什么(runCrawlSteps 的循环体)
|
||||||
|
|
||||||
```ts
|
每个 step 的处理流程如下(对每个 i 从 startIndex 到 steps.length-1):
|
||||||
export async function getDianshanCrawlState() {
|
|
||||||
return chrome.runtime.sendMessage(EXTENSION_ID, {
|
|
||||||
type: "DIANSHAN_GET_CRAWL_STATE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. 网站侧取消抓取
|
### 4.1 进入新 step:先更新状态机
|
||||||
|
1) 触发方法
|
||||||
|
- background:`updateCrawlTaskState(taskId, updater)`
|
||||||
|
|
||||||
```ts
|
2) 状态变化
|
||||||
export async function cancelDianshanCrawl() {
|
- `currentStepIndex = i`
|
||||||
return chrome.runtime.sendMessage(EXTENSION_ID, {
|
- `steps[i].status = 'running'`(其它步骤保持原样)
|
||||||
type: "DIANSHAN_CANCEL_CRAWL",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
取消后扩展会清空 `crawlTaskState`,并关闭扩展自动打开的浏览器窗口。
|
3) 通信影响
|
||||||
|
- 因为 `updateCrawlTaskState()` 内部会 `setCrawlTaskState()`:
|
||||||
|
- storage 写入 -> popup UI 更新
|
||||||
|
- 同时 `sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)` -> overlay 更新
|
||||||
|
- storage.onChanged -> external bridge 广播(如果外部网页连着)
|
||||||
|
|
||||||
## 7. 返回数据结构
|
### 4.2 跳转页面并等待加载完成
|
||||||
|
1) 触发方法
|
||||||
|
- background:`chrome.tabs.update(tabId, { url: step.url, active: true })`
|
||||||
|
- background:`waitForTabLoaded(tabId, signal)`
|
||||||
|
|
||||||
长连接收到 `DIANSHAN_CRAWL_DONE` 时,数据大致是:
|
2) 通信对象
|
||||||
|
- background -> Chrome tabs API(无 content 参与)
|
||||||
|
|
||||||
```ts
|
3) 状态变化
|
||||||
{
|
- 不改变任务状态,只是准备让 content script 进入目标页面上下文
|
||||||
ok: true,
|
|
||||||
type: "DIANSHAN_CRAWL_DONE",
|
|
||||||
data: {
|
|
||||||
state: {
|
|
||||||
id: "Shopee-...",
|
|
||||||
platformId: "Shopee",
|
|
||||||
platformName: "Shopee 后台",
|
|
||||||
status: "completed",
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
name: "数据看板",
|
|
||||||
uniqueKey: "databoard",
|
|
||||||
status: "success",
|
|
||||||
result: {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
databoard: {
|
|
||||||
name: "数据看板",
|
|
||||||
status: "success",
|
|
||||||
result: {}
|
|
||||||
},
|
|
||||||
adscenter: {
|
|
||||||
name: "广告中心",
|
|
||||||
status: "success",
|
|
||||||
result: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
网站项目里一般用 `message.data.result` 入库或展示即可;如果要展示进度,用 `message.data.state.steps`。
|
### 4.3 让 content script 执行抓取(核心通信)
|
||||||
|
1) 触发方法
|
||||||
|
- background:`scrapeStepInContent(tabId, step, signal)`
|
||||||
|
|
||||||
## 8. 最小使用流程
|
2) 通信
|
||||||
|
- background -> content(爬取窗口 tab):`chrome.tabs.sendMessage(tabId, { action:'SCRAPE_STEP', payload:{ fields, checkSelector } })`
|
||||||
|
|
||||||
```ts
|
3) content 入口
|
||||||
connectDianshanExtension();
|
- `content/pageRunner.ts` 的 `chrome.runtime.onMessage` 收到 `SCRAPE_STEP`
|
||||||
|
- `detectPageInterrupt()`:先判断是否需要人工处理(登录/验证码/404/未就绪)
|
||||||
|
- `waitForStableSelector(checkSelector, timeout)`:等待关键 DOM 稳定出现
|
||||||
|
- `processFields(fields, document.body)`:按配置抓取 DOM 数据
|
||||||
|
|
||||||
document.querySelector("#start")?.addEventListener("click", async () => {
|
4) 返回值约定(PageRunnerResponse)
|
||||||
await startDianshanCrawl("Shopee");
|
- `ok: true, data: DomScrapeResult`:本步骤抓取成功
|
||||||
});
|
- `interrupt: CrawlPauseInfo`:需要人工处理(会进入 paused)
|
||||||
```
|
- `ok: false, error`:未就绪/异常,background 会重试
|
||||||
|
|
||||||
注意:网站必须运行在 `externally_connectable.matches` 配置过的域名下,否则 Chrome 会拒绝调用扩展。
|
### 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`(完成时额外发送结果)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
resumeCrawl,
|
resumeCrawl,
|
||||||
startCrawl
|
startCrawl
|
||||||
} from "./task/crawlTask";
|
} from "./task/crawlTask";
|
||||||
import {getCrawlTaskState} from "./task/taskState";
|
import {getCrawlTaskState, updateCrawlTaskState} from "./task/taskState";
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
});
|
});
|
||||||
@@ -19,7 +19,7 @@ chrome.runtime.onStartup.addListener(() => {
|
|||||||
/**
|
/**
|
||||||
* 接受popup的指令
|
* 接受popup的指令
|
||||||
*/
|
*/
|
||||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
// 1. 统一提取 action 和 payload
|
// 1. 统一提取 action 和 payload
|
||||||
const action = message.action as MessageAction;
|
const action = message.action as MessageAction;
|
||||||
const payload = message.payload;
|
const payload = message.payload;
|
||||||
@@ -39,10 +39,30 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
resultData = await getCrawlTaskState();
|
resultData = await getCrawlTaskState();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "GET_CRAWL_STATE_FOR_TAB": {
|
||||||
|
// 中文备注:只允许“爬取窗口 tab”拿到状态,避免其它页面误显示悬浮面板。
|
||||||
|
const state = await getCrawlTaskState();
|
||||||
|
const senderTabId = sender.tab?.id;
|
||||||
|
resultData = state && senderTabId && state.tabId === senderTabId ? state : null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "CANCEL_CRAWL":
|
case "CANCEL_CRAWL":
|
||||||
await cancelCrawl()
|
await cancelCrawl()
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "CANCEL_AUTOCLOSE": {
|
||||||
|
// 中文备注:兼容旧悬浮面板按钮;当前版本默认完成后会清理并关闭窗口,这里仅做状态字段兼容。
|
||||||
|
const state = await getCrawlTaskState();
|
||||||
|
if (state) {
|
||||||
|
await updateCrawlTaskState(state.id, (s) => ({...s, autocloseAt: null}));
|
||||||
|
resultData = await getCrawlTaskState();
|
||||||
|
} else {
|
||||||
|
resultData = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "RESUME_CRAWL":
|
case "RESUME_CRAWL":
|
||||||
resultData = await resumeCrawl();
|
resultData = await resumeCrawl();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -47,11 +47,12 @@ export function mountCrawlOverlay(): void {
|
|||||||
refs.host.style.display = 'none';
|
refs.host.style.display = 'none';
|
||||||
maskHost = buildMaskHost();
|
maskHost = buildMaskHost();
|
||||||
|
|
||||||
// State broadcasts are targeted to the crawl tab only (background knows tabId).
|
// 中文备注:接收 background 通过 sendTabMessage(tabId, 'CRAWL_STATE_UPDATE', state) 推送的任务状态。
|
||||||
|
// 旧逻辑使用 type='crawl_state_update',这里改为适配当前的 action/payload 协议。
|
||||||
chrome.runtime.onMessage.addListener((raw) => {
|
chrome.runtime.onMessage.addListener((raw) => {
|
||||||
const msg = raw as { type?: string; state?: unknown } | undefined;
|
const msg = raw as { action?: string; payload?: unknown } | undefined;
|
||||||
if (msg?.type === 'crawl_state_update') {
|
if (msg?.action === 'CRAWL_STATE_UPDATE') {
|
||||||
applyState(isCrawlTaskState(msg.state) ? (msg.state as CrawlTaskState) : null);
|
applyState(isCrawlTaskState(msg.payload) ? (msg.payload as CrawlTaskState) : null);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@@ -114,14 +115,14 @@ function applyState(next: CrawlTaskState | null): void {
|
|||||||
// Mask blocks page interaction while running. Paused lifts mask so user can solve captcha/login.
|
// Mask blocks page interaction while running. Paused lifts mask so user can solve captcha/login.
|
||||||
setMaskActive(next.status === 'running');
|
setMaskActive(next.status === 'running');
|
||||||
|
|
||||||
// Auto-collapse once per task, only while actively running.
|
// 中文备注:默认折叠为左下角“圆形计时菜单”,用户点击后再展开查看进度。
|
||||||
if (!hasExpandedOnceForThisTask && next.status === 'running') {
|
if (!hasExpandedOnceForThisTask && next.status === 'running') {
|
||||||
hasExpandedOnceForThisTask = true;
|
hasExpandedOnceForThisTask = true;
|
||||||
setCollapsed(false);
|
setCollapsed(true);
|
||||||
if (autoCollapseTimer) window.clearTimeout(autoCollapseTimer);
|
if (autoCollapseTimer) {
|
||||||
autoCollapseTimer = window.setTimeout(() => {
|
window.clearTimeout(autoCollapseTimer);
|
||||||
if (currentState?.status === 'running') setCollapsed(true);
|
autoCollapseTimer = null;
|
||||||
}, 3000);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next.status === 'paused') {
|
if (next.status === 'paused') {
|
||||||
@@ -230,10 +231,16 @@ function render(state: CrawlTaskState): void {
|
|||||||
refs.autocloseBanner.style.display = 'none';
|
refs.autocloseBanner.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capsule text
|
// Capsule text
|
||||||
const capsuleText = refs.capsule.querySelector('.capsule-text') as HTMLSpanElement | null;
|
// 中文备注:圆形菜单上只显示计时(mm:ss)。
|
||||||
if (capsuleText) {
|
const capsuleTime = refs.capsule.querySelector('.capsule-time') as HTMLSpanElement | null;
|
||||||
capsuleText.textContent =
|
if (capsuleTime) {
|
||||||
|
capsuleTime.textContent = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
const capsuleText = refs.capsule.querySelector('.capsule-text') as HTMLSpanElement | null;
|
||||||
|
if (capsuleText) {
|
||||||
|
capsuleText.textContent =
|
||||||
phase === 'paused'
|
phase === 'paused'
|
||||||
? '已暂停'
|
? '已暂停'
|
||||||
: isTerminal(state.status)
|
: isTerminal(state.status)
|
||||||
@@ -281,7 +288,8 @@ function buildDom(): OverlayRefs {
|
|||||||
host.id = OVERLAY_HOST_ID;
|
host.id = OVERLAY_HOST_ID;
|
||||||
host.style.all = 'initial';
|
host.style.all = 'initial';
|
||||||
host.style.position = 'fixed';
|
host.style.position = 'fixed';
|
||||||
host.style.right = '24px';
|
// 中文备注:爬取窗口左下角放圆形菜单(计时 + 入口)
|
||||||
|
host.style.left = '24px';
|
||||||
host.style.bottom = '24px';
|
host.style.bottom = '24px';
|
||||||
host.style.zIndex = '2147483647';
|
host.style.zIndex = '2147483647';
|
||||||
|
|
||||||
@@ -335,7 +343,7 @@ function buildDom(): OverlayRefs {
|
|||||||
|
|
||||||
<button class="capsule" type="button" aria-label="展开面板">
|
<button class="capsule" type="button" aria-label="展开面板">
|
||||||
<div class="radar-mini"><div class="sweep-mini"></div></div>
|
<div class="radar-mini"><div class="sweep-mini"></div></div>
|
||||||
<span class="capsule-text">爬取中</span>
|
<span class="capsule-time">爬取中</span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -597,10 +605,11 @@ function styleTag(): string {
|
|||||||
.cancel-btn { align-self: flex-start; padding: 5px 10px; background: transparent; color: #8b949e; border: 1px solid #30363d; border-radius: 6px; font-size: 11px; cursor: pointer; transition: all 120ms ease; }
|
.cancel-btn { align-self: flex-start; padding: 5px 10px; background: transparent; color: #8b949e; border: 1px solid #30363d; border-radius: 6px; font-size: 11px; cursor: pointer; transition: all 120ms ease; }
|
||||||
.cancel-btn:hover { color: #f85149; border-color: #f85149; }
|
.cancel-btn:hover { color: #f85149; border-color: #f85149; }
|
||||||
|
|
||||||
.capsule { display: inline-flex; align-items: center; gap: 8px; padding: 7px 12px 7px 8px; background: #0d1117; color: #e6edf3; border: 1px solid #30363d; border-radius: 999px; box-shadow: 0 4px 14px rgba(0,0,0,0.4); cursor: pointer; font-size: 12px; transition: all 120ms ease; }
|
/* 中文备注:圆形菜单(左下角)只展示计时;点击展开查看详细进度 */
|
||||||
.capsule:hover { transform: translateY(-1px); }
|
.capsule { width: 56px; height: 56px; padding: 0; display: inline-flex; align-items: center; justify-content: center; background: #0d1117; color: #e6edf3; border: 1px solid #30363d; border-radius: 50%; box-shadow: 0 4px 14px rgba(0,0,0,0.4); cursor: pointer; font-size: 12px; transition: all 120ms ease; position: relative; }
|
||||||
.radar-mini { width: 18px; height: 18px; border-radius: 50%; position: relative; overflow: hidden; background: radial-gradient(circle at center, rgba(46,160,67,0.2), rgba(46,160,67,0.02) 70%, transparent 80%); border: 1px solid rgba(46,160,67,0.4); }
|
.capsule:hover { transform: translateY(-1px); }
|
||||||
.sweep-mini { position: absolute; inset: 0; background: conic-gradient(from 0deg, rgba(46,160,67,0) 0deg, rgba(46,160,67,0.8) 50deg, rgba(46,160,67,0) 60deg); animation: sweep 2s linear infinite; }
|
.radar-mini { position: absolute; inset: 6px; border-radius: 50%; overflow: hidden; background: radial-gradient(circle at center, rgba(46,160,67,0.20), rgba(46,160,67,0.03) 70%, transparent 82%); border: 1px solid rgba(46,160,67,0.40); }
|
||||||
.capsule-text { white-space: nowrap; }
|
.sweep-mini { position: absolute; inset: 0; background: conic-gradient(from 0deg, rgba(46,160,67,0) 0deg, rgba(46,160,67,0.8) 50deg, rgba(46,160,67,0) 60deg); animation: sweep 2s linear infinite; }
|
||||||
</style>`;
|
.capsule-time { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 700; letter-spacing: 0.2px; }
|
||||||
}
|
</style>`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import {platformConfigs} from '@/config/platforms';
|
|||||||
import type {CrawlTaskState} from '@/types';
|
import type {CrawlTaskState} from '@/types';
|
||||||
import {sendBackgroundMessage} from '@/shared/message';
|
import {sendBackgroundMessage} from '@/shared/message';
|
||||||
|
|
||||||
/** 用于同步爬取任务状态的 `chrome.storage.local` key。 */
|
|
||||||
const CRAWL_TASK_STORAGE_KEY = 'crawlTaskState';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Popup 内的爬取状态与操作集合。
|
* Popup 内的爬取状态与操作集合。
|
||||||
@@ -148,7 +146,7 @@ export const useScan = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const change = changes[CRAWL_TASK_STORAGE_KEY];
|
const change = changes["crawlTaskState"];
|
||||||
|
|
||||||
if (!change) {
|
if (!change) {
|
||||||
return;
|
return;
|
||||||
@@ -161,19 +159,14 @@ export const useScan = () => {
|
|||||||
/** 首次加载 + 订阅 storage 事件。 */
|
/** 首次加载 + 订阅 storage 事件。 */
|
||||||
await refreshCrawlState();
|
await refreshCrawlState();
|
||||||
|
|
||||||
if (typeof chrome !== 'undefined' && chrome.storage?.onChanged) {
|
chrome.storage.onChanged.addListener(handleStorageChanged);
|
||||||
chrome.storage.onChanged.addListener(handleStorageChanged);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
/** 清理计时器 + 取消订阅 storage 事件。 */
|
/** 清理计时器 + 取消订阅 storage 事件。 */
|
||||||
// 中文备注:统一走 stopElapsedTimer,避免 timer 残留。
|
|
||||||
stopElapsedTimer();
|
stopElapsedTimer();
|
||||||
|
|
||||||
if (typeof chrome !== 'undefined' && chrome.storage?.onChanged) {
|
chrome.storage.onChanged.removeListener(handleStorageChanged);
|
||||||
chrome.storage.onChanged.removeListener(handleStorageChanged);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ export type MessageAction =
|
|||||||
/** 获取当前爬取任务的状态*/
|
/** 获取当前爬取任务的状态*/
|
||||||
| 'GET_CRAWL_STATE'
|
| 'GET_CRAWL_STATE'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅供 content script(爬取窗口页内悬浮面板)拉取状态:
|
||||||
|
* - 只在“当前 tab == 爬取 tab”时返回状态,其它 tab 返回 null。
|
||||||
|
* 中文备注:用于悬浮面板初始化时拿到任务快照。
|
||||||
|
*/
|
||||||
|
| 'GET_CRAWL_STATE_FOR_TAB'
|
||||||
|
|
||||||
/** 启动一个新的爬取任务 */
|
/** 启动一个新的爬取任务 */
|
||||||
| 'START_CRAWL'
|
| 'START_CRAWL'
|
||||||
|
|
||||||
@@ -11,6 +18,12 @@ export type MessageAction =
|
|||||||
/** 恢复之前被暂停或因中断而停止的爬取任务 */
|
/** 恢复之前被暂停或因中断而停止的爬取任务 */
|
||||||
| 'RESUME_CRAWL'
|
| 'RESUME_CRAWL'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消自动关闭(旧悬浮面板按钮会发这个指令)。
|
||||||
|
* 中文备注:当前版本默认完成后会清理并关闭窗口;这里保留用于兼容,避免后台报“未知指令”。
|
||||||
|
*/
|
||||||
|
| 'CANCEL_AUTOCLOSE'
|
||||||
|
|
||||||
|
|
||||||
/** 忽略/关闭当前爬取任务的 UI 提示或通知(通常指任务结束后清理界面) */
|
/** 忽略/关闭当前爬取任务的 UI 提示或通知(通常指任务结束后清理界面) */
|
||||||
| 'DISMISS_CRAWL';
|
| 'DISMISS_CRAWL';
|
||||||
@@ -38,4 +51,4 @@ export function sendBackgroundMessage<T>(data: BackgroundMessage): Promise<Backg
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//接受
|
//接受
|
||||||
|
|||||||
0
src/shared/request.ts
Normal file
0
src/shared/request.ts
Normal file
@@ -1 +1 @@
|
|||||||
{"root":["./manifest.config.ts","./message.js","./vite.config.ts","./src/background/domscraper.ts","./src/background/index.ts","./src/background/types.ts","./src/background/service/externalbridge.ts","./src/background/task/crawltask.ts","./src/background/task/helper.ts","./src/background/task/taskstate.ts","./src/config/platforms.ts","./src/content/app.vue","./src/content/crawloverlay.ts","./src/content/main.ts","./src/content/pagerunner.ts","./src/options/app.vue","./src/options/main.ts","./src/popup/app.vue","./src/popup/main.ts","./src/popup/hook/use-i18n.ts","./src/popup/hook/use-login.ts","./src/popup/hook/use-scan.ts","./src/shared/auth.ts","./src/shared/message.ts","./src/shared/tab.ts","./src/shared/time_format.ts","./src/types/crawl.ts","./src/types/index.ts","./src/types/platform.ts"],"version":"5.9.3"}
|
{"root":["./manifest.config.ts","./message.js","./vite.config.ts","./src/background/domscraper.ts","./src/background/index.ts","./src/background/types.ts","./src/background/service/externalbridge.ts","./src/background/task/crawltask.ts","./src/background/task/helper.ts","./src/background/task/taskstate.ts","./src/config/platforms.ts","./src/content/app.vue","./src/content/crawloverlay.ts","./src/content/main.ts","./src/content/pagerunner.ts","./src/options/app.vue","./src/options/main.ts","./src/popup/app.vue","./src/popup/main.ts","./src/popup/hook/use-i18n.ts","./src/popup/hook/use-login.ts","./src/popup/hook/use-scan.ts","./src/shared/auth.ts","./src/shared/message.ts","./src/shared/request.ts","./src/shared/tab.ts","./src/shared/time_format.ts","./src/types/crawl.ts","./src/types/index.ts","./src/types/platform.ts"],"version":"5.9.3"}
|
||||||
661
we.md
661
we.md
@@ -1,661 +0,0 @@
|
|||||||
# 店闪扩展执行链路说明
|
|
||||||
|
|
||||||
这份文档按当前项目代码整理,目的是方便顺着代码阅读整个爬取流程。
|
|
||||||
|
|
||||||
## 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`:最后看数据结构。
|
|
||||||
Reference in New Issue
Block a user