From 53e4f0b2f47c8579e0712ae2876deae67b22ad2e Mon Sep 17 00:00:00 2001 From: zhu <1812073942@qq.com> Date: Wed, 6 May 2026 10:22:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/background/domScraper.ts | 28 ++++-- src/background/index.ts | 6 +- src/background/service.ts | 172 ++++++++++++++++++++++++----------- src/background/types.ts | 28 +++--- src/config/platforms.ts | 6 +- src/content/App.vue | 44 +++++---- src/content/main.ts | 11 ++- src/shared/auth.ts | 16 +++- src/types/crawl.ts | 40 ++++---- src/types/platform.ts | 88 ++++++++---------- 10 files changed, 256 insertions(+), 183 deletions(-) diff --git a/src/background/domScraper.ts b/src/background/domScraper.ts index 8e1c1c2..e558c87 100644 --- a/src/background/domScraper.ts +++ b/src/background/domScraper.ts @@ -1,6 +1,6 @@ -import type { PlatformFieldConfig } from '@/types'; +import type { PlatformFieldConfig } from '@/types'; -/** DOM 抓取后的通用结果结构。 */ +// DOM 抓取后的通用结果结构。 export type DomScrapeResult = Record; @@ -19,10 +19,12 @@ export async function scrapeDomFields(fields: PlatformFieldConfig[]): Promise new Promise((resolve) => window.setTimeout(resolve, ms ?? 1500)); -/** 从元素中提取实际值,默认取文本,也支持 attr、图片 src、链接 href。 */ +/** + * 从元素中提取实际值,默认取文本,也支持 attr、图片 src、链接 href。 + */ function extractValue(el: Element | null, config: PlatformFieldConfig): string | null { if (!el) { return null; @@ -46,7 +48,9 @@ function extractValue(el: Element | null, config: PlatformFieldConfig): string | return (el.textContent || '').replace(/\n/g, '').trim(); } -/** 根据字段 condition 配置在指定 DOM 范围内自动点击目标元素。 */ +/** + * 根据字段 condition 配置在指定 DOM 范围内自动点击目标元素。 + */ async function autoClick(config: PlatformFieldConfig, rootDom: ParentNode): Promise { if (!config.condition) { return; @@ -62,7 +66,9 @@ async function autoClick(config: PlatformFieldConfig, rootDom: ParentNode): Prom } } -/** 递归处理字段配置,支持普通字段、嵌套 row、列表和表格。 */ +/** + * 递归处理字段配置,支持普通字段、嵌套 row、列表和表格。 + */ async function processFields(columns: PlatformFieldConfig[], rootDom: ParentNode): Promise { const result: DomScrapeResult = {}; @@ -100,7 +106,9 @@ async function processFields(columns: PlatformFieldConfig[], rootDom: ParentNode return result; } -/** 按列表配置抓取所有列表项,并按分页配置继续翻页。 */ +/** + * 按列表配置抓取所有列表项,并按分页配置继续翻页。 + */ async function processList(config: PlatformFieldConfig, rootDom: ParentNode): Promise { const allList: DomScrapeResult[] = []; let pageCount = 0; @@ -148,7 +156,9 @@ async function processList(config: PlatformFieldConfig, rootDom: ParentNode): Pr return allList; } -/** 按表格配置抓取表格行数据,并按分页配置继续翻页。 */ +/** + * 按表格配置抓取表格行数据,并按分页配置继续翻页。 + */ async function processTable(config: PlatformFieldConfig, rootDom: ParentNode): Promise { const allTableData: DomScrapeResult[] = []; let pageCount = 0; @@ -221,4 +231,4 @@ async function processTable(config: PlatformFieldConfig, rootDom: ParentNode): P } return allTableData; -} \ No newline at end of file +} diff --git a/src/background/index.ts b/src/background/index.ts index cb5590d..3165e36 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,4 +1,4 @@ -import { handleBackgroundCommand, handleInstalled, handleStartup, handleWindowRemoved } from './service'; +import { handleBackgroundCommand, handleInstalled, handleStartup, handleWindowRemoved } from './service'; import type { BackgroundCommand } from './types'; chrome.runtime.onInstalled.addListener(() => { @@ -18,7 +18,9 @@ chrome.windows.onRemoved.addListener((windowId) => { void handleWindowRemoved(windowId); }); -/** 统一包装后台消息处理,确保异步错误能回给调用方。 */ +/** + * 统一包装后台消息处理,确保异步错误能回给调用方。 + */ async function handleBackgroundMessage( message: BackgroundCommand, sendResponse: (response?: unknown) => void, diff --git a/src/background/service.ts b/src/background/service.ts index 24fca41..3f4ab51 100644 --- a/src/background/service.ts +++ b/src/background/service.ts @@ -1,26 +1,32 @@ -import { getPlatformById } from '@/config/platforms'; +import { getPlatformById } from '@/config/platforms'; import type { CrawlProgressStep, CrawlTaskState, PlatformConfig, PlatformStepConfig } from '@/types'; import { scrapeDomFields, type DomScrapeResult } from './domScraper'; import type { BackgroundCommand, BackgroundResponse, CrawlStateResponse } from './types'; -/** chrome.storage.local 中保存当前爬取任务状态的键名。 */ +// chrome.storage.local 中保存当前爬取任务状态的键名。 const CRAWL_TASK_STORAGE_KEY = 'crawlTaskState'; -/** 扩展安装完成时的初始化入口,当前仅保留日志方便调试生命周期。 */ +/** + * 扩展安装完成时的初始化入口,当前仅保留日志方便调试生命周期。 + */ export async function handleInstalled(): Promise { console.log('[background] installed'); } -/** 浏览器启动并加载扩展时的初始化入口,当前仅保留日志方便调试生命周期。 */ +/** + * 浏览器启动并加载扩展时的初始化入口,当前仅保留日志方便调试生命周期。 + */ export async function handleStartup(): Promise { console.log('[background] startup'); } -/** 监听窗口关闭事件;如果关闭的是爬取窗口,就把当前任务标记为取消。 */ +/** + * 监听窗口关闭事件;如果关闭的是爬取窗口,就把当前任务标记为取消。 + */ export async function handleWindowRemoved(windowId: number): Promise { console.log('[background] window removed', windowId); - /** 当前保存的爬取任务状态。 */ + // 当前保存的爬取任务状态。 const state = await getCrawlTaskState(); if (state?.windowId === windowId && state.status === 'running') { @@ -34,7 +40,9 @@ export async function handleWindowRemoved(windowId: number): Promise { } } -/** 根据 popup/content 发来的 action 分发到对应的后台处理函数。 */ +/** + * 根据 popup/content 发来的 action 分发到对应的后台处理函数。 + */ export async function handleBackgroundCommand( message: BackgroundCommand, ): Promise { @@ -50,9 +58,11 @@ export async function handleBackgroundCommand( } } -/** 创建新的爬取任务,打开目标平台窗口,并把初始时间轴状态写入 storage。 */ +/** + * 创建新的爬取任务,打开目标平台窗口,并把初始时间轴状态写入 storage。 + */ async function startCrawl(platformId: string): Promise { - /** 根据平台 ID 找到对应的平台爬取配置。 */ + // 根据平台 ID 找到对应的平台爬取配置。 const platform = getPlatformById(platformId); if (!platform) { @@ -65,9 +75,10 @@ async function startCrawl(platformId: string): Promise { return { ok: false, error: '平台未配置爬取步骤' }; } - /** 当前任务的开始时间戳,用于计算正计时。 */ + // 当前任务的开始时间戳,用于计算正计时。 const startedAt = Date.now(); - /** 窗口创建前的初始任务状态,先写入 storage 让所有页面能立即感知爬取开始。 */ + // 窗口创建前的初始任务状态。 + // 先写入 storage 让所有页面能立即感知爬取开始。 const nextState: CrawlTaskState = { id: `${platform.id}-${startedAt}`, platformId: platform.id, @@ -85,15 +96,17 @@ async function startCrawl(platformId: string): Promise { await setCrawlTaskState(nextState); try { - /** background 创建出来的目标平台窗口信息。 */ + // background 创建出来的目标平台窗口信息。 const windowInfo = await createCrawlWindow(firstStep.url); - /** 补充 windowId 后的任务状态,后续可用于取消或监听窗口关闭。 */ + // 补充 windowId 后的任务状态。 + // 后续可用于取消或监听窗口关闭。 const stateWithWindow = { ...nextState, windowId: windowInfo.id }; await setCrawlTaskState(stateWithWindow); void runCrawlSteps(platform, stateWithWindow); return { ok: true, data: stateWithWindow }; } catch (error: unknown) { - /** 窗口创建失败时写入的失败状态,供 popup/content 显示错误进度。 */ + // 窗口创建失败时写入的失败状态。 + // 供 popup/content 显示错误进度。 const failedState: CrawlTaskState = { ...nextState, status: 'failed', @@ -106,18 +119,21 @@ async function startCrawl(platformId: string): Promise { } } -/** 按平台 steps 顺序执行页面跳转、DOM 等待、字段抓取和进度更新。 */ +/** + * 按平台 steps 顺序执行页面跳转、DOM 等待、字段抓取和进度更新。 + */ async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskState): Promise { if (!initialState.windowId) { return; } try { - /** 新窗口中的目标标签页 ID,后续所有跳转和脚本注入都依赖它。 */ + // 新窗口中的目标标签页 ID。 + // 后续所有跳转和脚本注入都依赖它。 const tabId = await getWindowActiveTabId(initialState.windowId); for (let stepIndex = 0; stepIndex < platform.steps.length; stepIndex += 1) { - /** 当前正在执行的平台页面步骤配置。 */ + // 当前正在执行的平台页面步骤配置。 const step = platform.steps[stepIndex]; if (!(await isTaskRunning(initialState.id))) { @@ -127,7 +143,7 @@ async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskSt await markStepRunning(initialState.id, stepIndex); await openStepPage(tabId, step.url); - /** 当前页面核心 DOM 是否已经出现。 */ + // 当前页面核心 DOM 是否已经出现。 const isReady = await waitForStepReady(tabId, step); if (!isReady) { @@ -136,7 +152,7 @@ async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskSt return; } - /** 注入页面执行后的字段抓取结果。 */ + // 注入页面执行后的字段抓取结果。 const data = await scrapeStepFields(tabId, step); console.log(`[crawl] ${platform.name} - ${step.name} 提取成功`, data); await markStepSuccess(initialState.id, stepIndex); @@ -149,11 +165,13 @@ async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskSt } } -/** 获取指定窗口中的活动 tab ID。 */ +/** + * 获取指定窗口中的活动 tab ID。 + */ async function getWindowActiveTabId(windowId: number): Promise { - /** 指定窗口中查询到的标签页列表。 */ + // 指定窗口中查询到的标签页列表。 const tabs = await chrome.tabs.query({ windowId, active: true }); - /** 当前窗口里用于承载爬取页面的活动标签页。 */ + // 当前窗口里用于承载爬取页面的活动标签页。 const tab = tabs[0]; if (!tab?.id) { @@ -163,22 +181,28 @@ async function getWindowActiveTabId(windowId: number): Promise { return tab.id; } -/** 打开某个 steps 页面,并等待浏览器报告 tab 加载完成。 */ +/** + * 打开某个 steps 页面,并等待浏览器报告 tab 加载完成。 + */ async function openStepPage(tabId: number, url: string): Promise { await chrome.tabs.update(tabId, { url, active: true }); await waitForTabLoaded(tabId); } -/** 等待 tab 完成页面加载。 */ +/** + * 等待 tab 完成页面加载。 + */ function waitForTabLoaded(tabId: number): Promise { return new Promise((resolve) => { - /** 页面加载兜底定时器,避免某些站点不触发 complete 时流程永久挂起。 */ + // 页面加载兜底定时器。 + // 避免某些站点不触发 complete 时流程永久挂起。 const timeout = globalThis.setTimeout(() => { chrome.tabs.onUpdated.removeListener(handleUpdated); resolve(); }, 15000); - /** chrome.tabs.onUpdated 的监听器,用于捕获指定 tab 的 complete 状态。 */ + // chrome.tabs.onUpdated 的监听器。 + // 用于捕获指定 tab 的 complete 状态。 function handleUpdated(updatedTabId: number, changeInfo: { status?: string }) { if (updatedTabId === tabId && changeInfo.status === 'complete') { globalThis.clearTimeout(timeout); @@ -191,7 +215,9 @@ function waitForTabLoaded(tabId: number): Promise { }); } -/** 等待步骤配置中的 checkSelector 出现;第一次超时后刷新页面再重试一次。 */ +/** + * 等待步骤配置中的 checkSelector 出现;第一次超时后刷新页面再重试一次。 + */ async function waitForStepReady(tabId: number, step: PlatformStepConfig): Promise { if (await waitForSelector(tabId, step.checkSelector, 5000)) { return true; @@ -202,13 +228,15 @@ async function waitForStepReady(tabId: number, step: PlatformStepConfig): Promis return waitForSelector(tabId, step.checkSelector, 5000); } -/** 在目标页面轮询检查指定 selector 是否存在。 */ +/** + * 在目标页面轮询检查指定 selector 是否存在。 + */ async function waitForSelector(tabId: number, selector: string, timeoutMs: number): Promise { - /** 轮询开始时间,用于控制最大等待时长。 */ + // 轮询开始时间,用于控制最大等待时长。 const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { - /** 当前页面是否已经能查询到目标元素。 */ + // 当前页面是否已经能查询到目标元素。 const exists = await checkSelectorExists(tabId, selector); if (exists) { @@ -221,9 +249,11 @@ async function waitForSelector(tabId: number, selector: string, timeoutMs: numbe return false; } -/** 注入轻量脚本检查页面里是否存在指定 selector。 */ +/** + * 注入轻量脚本检查页面里是否存在指定 selector。 + */ async function checkSelectorExists(tabId: number, selector: string): Promise { - /** chrome.scripting.executeScript 返回的注入执行结果。 */ + // chrome.scripting.executeScript 返回的注入执行结果。 const results = await chrome.scripting.executeScript({ target: { tabId }, func: (targetSelector: string) => Boolean(document.querySelector(targetSelector)), @@ -233,9 +263,11 @@ async function checkSelectorExists(tabId: number, selector: string): Promise { - /** 目标页面执行 DOM 抓取后返回的结果数组。 */ + // 目标页面执行 DOM 抓取后返回的结果数组。 const results = await chrome.scripting.executeScript({ target: { tabId }, func: scrapeDomFields, @@ -245,14 +277,18 @@ async function scrapeStepFields(tabId: number, step: PlatformStepConfig): Promis return results[0]?.result ?? null; } -/** 判断指定任务是否仍处于 running 状态。 */ +/** + * 判断指定任务是否仍处于 running 状态。 + */ async function isTaskRunning(taskId: string): Promise { - /** 当前 storage 中的任务状态。 */ + // 当前 storage 中的任务状态。 const state = await getCrawlTaskState(); return state?.id === taskId && state.status === 'running'; } -/** 将指定步骤标记为运行中,同时把其它未完成步骤保持为等待。 */ +/** + * 将指定步骤标记为运行中,同时把其它未完成步骤保持为等待。 + */ async function markStepRunning(taskId: string, stepIndex: number): Promise { await updateCrawlTaskState(taskId, (state) => ({ ...state, @@ -266,7 +302,9 @@ async function markStepRunning(taskId: string, stepIndex: number): Promise })); } -/** 将指定步骤标记为成功。 */ +/** + * 将指定步骤标记为成功。 + */ async function markStepSuccess(taskId: string, stepIndex: number): Promise { await updateCrawlTaskState(taskId, (state) => ({ ...state, @@ -276,7 +314,9 @@ async function markStepSuccess(taskId: string, stepIndex: number): Promise })); } -/** 将指定步骤标记为失败,并记录失败原因。 */ +/** + * 将指定步骤标记为失败,并记录失败原因。 + */ async function markStepFailed(taskId: string, stepIndex: number, message: string): Promise { await updateCrawlTaskState(taskId, (state) => ({ ...state, @@ -287,7 +327,9 @@ async function markStepFailed(taskId: string, stepIndex: number, message: string })); } -/** 将整个任务标记为完成。 */ +/** + * 将整个任务标记为完成。 + */ async function markTaskCompleted(taskId: string): Promise { await updateCrawlTaskState(taskId, (state) => ({ ...state, @@ -296,7 +338,9 @@ async function markTaskCompleted(taskId: string): Promise { })); } -/** 将整个任务标记为失败。 */ +/** + * 将整个任务标记为失败。 + */ async function markTaskFailed(taskId: string, message = '爬取失败'): Promise { await updateCrawlTaskState(taskId, (state) => ({ ...state, @@ -307,12 +351,14 @@ async function markTaskFailed(taskId: string, message = '爬取失败'): Promise })); } -/** 读取任务状态后执行不可变更新,避免覆盖已取消或已替换的任务。 */ +/** + * 读取任务状态后执行不可变更新,避免覆盖已取消或已替换的任务。 + */ async function updateCrawlTaskState( taskId: string, updater: (state: CrawlTaskState) => CrawlTaskState, ): Promise { - /** 当前 storage 中最新的任务状态。 */ + // 当前 storage 中最新的任务状态。 const state = await getCrawlTaskState(); if (!state || state.id !== taskId || state.status === 'canceled') { @@ -322,23 +368,28 @@ async function updateCrawlTaskState( await setCrawlTaskState(updater(state)); } -/** 睡眠工具,用于轮询 DOM 等待。 */ +/** + * 睡眠工具,用于轮询 DOM 等待。 + */ function sleep(ms: number): Promise { return new Promise((resolve) => { globalThis.setTimeout(resolve, ms); }); } -/** 取消当前爬取任务,并尝试关闭正在爬取的平台窗口。 */ +/** + * 取消当前爬取任务,并尝试关闭正在爬取的平台窗口。 + */ async function cancelCrawl(): Promise { - /** 当前保存的爬取任务状态。 */ + // 当前保存的爬取任务状态。 const state = await getCrawlTaskState(); if (!state) { return { ok: true, data: null }; } - /** 用户取消后的任务状态,当前执行步骤会显示为失败并附带取消原因。 */ + // 用户取消后的任务状态。 + // 当前执行步骤会显示为失败并附带取消原因。 const canceledState: CrawlTaskState = { ...state, status: 'canceled', @@ -356,21 +407,28 @@ async function cancelCrawl(): Promise { return { ok: true, data: canceledState }; } -/** 从 chrome.storage.local 读取当前爬取任务状态。 */ +/** + * 从 chrome.storage.local 读取当前爬取任务状态。 + */ async function getCrawlTaskState(): Promise { - /** chrome.storage.local 返回的原始键值对象。 */ + // chrome.storage.local 返回的原始键值对象。 const result = await chrome.storage.local.get(CRAWL_TASK_STORAGE_KEY); - /** 取出的任务状态候选值,需要经过结构校验后才能使用。 */ + // 取出的任务状态候选值。 + // 需要经过结构校验后才能使用。 const state = result[CRAWL_TASK_STORAGE_KEY]; return isCrawlTaskState(state) ? state : null; } -/** 将最新爬取任务状态写入 chrome.storage.local,供 popup 和 content script 同步读取。 */ +/** + * 将最新爬取任务状态写入 chrome.storage.local,供 popup 和 content script 同步读取。 + */ async function setCrawlTaskState(state: CrawlTaskState): Promise { await chrome.storage.local.set({ [CRAWL_TASK_STORAGE_KEY]: state }); } -/** 打开一个普通浏览器窗口承载目标平台页面。 */ +/** + * 打开一个普通浏览器窗口承载目标平台页面。 + */ function createCrawlWindow(url: string): Promise { return new Promise((resolve, reject) => { chrome.windows.create( @@ -382,7 +440,7 @@ function createCrawlWindow(url: string): Promise { height: 900, }, (windowInfo) => { - /** Chrome 扩展 API 回调中的运行时错误。 */ + // Chrome 扩展 API 回调中的运行时错误。 const runtimeError = chrome.runtime.lastError; if (runtimeError) { @@ -401,7 +459,9 @@ function createCrawlWindow(url: string): Promise { }); } -/** 根据窗口 ID 关闭爬取窗口;关闭失败时不阻塞取消状态写入。 */ +/** + * 根据窗口 ID 关闭爬取窗口;关闭失败时不阻塞取消状态写入。 + */ function removeWindow(windowId: number): Promise { return new Promise((resolve) => { chrome.windows.remove(windowId, () => { @@ -410,7 +470,9 @@ function removeWindow(windowId: number): Promise { }); } -/** 粗略判断 storage 中读取到的值是否像一个爬取任务状态对象。 */ +/** + * 粗略判断 storage 中读取到的值是否像一个爬取任务状态对象。 + */ function isCrawlTaskState(value: unknown): value is CrawlTaskState { return typeof value === 'object' && value !== null && 'id' in value && 'steps' in value; } diff --git a/src/background/types.ts b/src/background/types.ts index 9e9e196..0452880 100644 --- a/src/background/types.ts +++ b/src/background/types.ts @@ -1,40 +1,40 @@ import type { CrawlTaskState } from '@/types'; -/** 启动爬取任务的后台消息。 */ +// 启动爬取任务的后台消息。 export interface StartCrawlCommand { - /** 消息动作类型:请求 background 创建爬取窗口并初始化任务状态。 */ + // 消息动作类型:请求 background 创建爬取窗口并初始化任务状态。 action: 'START_CRAWL'; - /** 启动爬取所需参数。 */ + // 启动爬取所需参数。 payload: { - /** 当前要爬取的平台 ID,对应 config/platforms.ts 中的平台配置。 */ + // 当前要爬取的平台 ID,对应 config/platforms.ts 中的平台配置。 platformId: string; }; } -/** 获取当前爬取任务状态的后台消息。 */ +// 获取当前爬取任务状态的后台消息。 export interface GetCrawlStateCommand { - /** 消息动作类型:请求 background 返回当前任务快照。 */ + // 消息动作类型:请求 background 返回当前任务快照。 action: 'GET_CRAWL_STATE'; } -/** 取消当前爬取任务的后台消息。 */ +// 取消当前爬取任务的后台消息。 export interface CancelCrawlCommand { - /** 消息动作类型:请求 background 标记任务取消并关闭爬取窗口。 */ + // 消息动作类型:请求 background 标记任务取消并关闭爬取窗口。 action: 'CANCEL_CRAWL'; } -/** popup/content script 能发送给 background 的全部消息类型。 */ +// popup/content script 能发送给 background 的全部消息类型。 export type BackgroundCommand = StartCrawlCommand | GetCrawlStateCommand | CancelCrawlCommand; -/** background 统一响应结构。 */ +// background 统一响应结构。 export interface BackgroundResponse { - /** 当前请求是否处理成功。 */ + // 当前请求是否处理成功。 ok: boolean; - /** 成功或部分失败时返回的业务数据。 */ + // 成功或部分失败时返回的业务数据。 data?: T; - /** 请求失败时返回的错误文案。 */ + // 请求失败时返回的错误文案。 error?: string; } -/** 获取或变更爬取任务后返回的响应结构。 */ +// 获取或变更爬取任务后返回的响应结构。 export type CrawlStateResponse = BackgroundResponse; diff --git a/src/config/platforms.ts b/src/config/platforms.ts index 9401964..b221b4f 100644 --- a/src/config/platforms.ts +++ b/src/config/platforms.ts @@ -1,4 +1,4 @@ - + import type { PlatformConfig } from '@/types'; export const PLATFORM_CONFIGS: PlatformConfig[] = [ @@ -123,7 +123,9 @@ export const PLATFORM_CONFIGS: PlatformConfig[] = [ }, ] -/** 根据平台 ID 返回对应的平台抓取配置。 */ +/** + * 根据平台 ID 返回对应的平台抓取配置。 + */ export function getPlatformById(platformId: string) { return PLATFORM_CONFIGS.find((item) => item.id === platformId) ?? null; } diff --git a/src/content/App.vue b/src/content/App.vue index 882e389..d3c797f 100644 --- a/src/content/App.vue +++ b/src/content/App.vue @@ -1,20 +1,20 @@ -