注释
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import type { PlatformFieldConfig } from '@/types';
|
import type { PlatformFieldConfig } from '@/types';
|
||||||
|
|
||||||
/** DOM 抓取后的通用结果结构。 */
|
// DOM 抓取后的通用结果结构。
|
||||||
export type DomScrapeResult = Record<string, unknown>;
|
export type DomScrapeResult = Record<string, unknown>;
|
||||||
|
|
||||||
|
|
||||||
@@ -19,10 +19,12 @@ export async function scrapeDomFields(fields: PlatformFieldConfig[]): Promise<Do
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** 睡眠工具,给点击、翻页、异步渲染留出等待时间。 */
|
// 睡眠工具,给点击、翻页、异步渲染留出等待时间。
|
||||||
const sleep = (ms?: number) => new Promise((resolve) => window.setTimeout(resolve, ms ?? 1500));
|
const sleep = (ms?: number) => new Promise((resolve) => window.setTimeout(resolve, ms ?? 1500));
|
||||||
|
|
||||||
/** 从元素中提取实际值,默认取文本,也支持 attr、图片 src、链接 href。 */
|
/**
|
||||||
|
* 从元素中提取实际值,默认取文本,也支持 attr、图片 src、链接 href。
|
||||||
|
*/
|
||||||
function extractValue(el: Element | null, config: PlatformFieldConfig): string | null {
|
function extractValue(el: Element | null, config: PlatformFieldConfig): string | null {
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return null;
|
return null;
|
||||||
@@ -46,7 +48,9 @@ function extractValue(el: Element | null, config: PlatformFieldConfig): string |
|
|||||||
return (el.textContent || '').replace(/\n/g, '').trim();
|
return (el.textContent || '').replace(/\n/g, '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据字段 condition 配置在指定 DOM 范围内自动点击目标元素。 */
|
/**
|
||||||
|
* 根据字段 condition 配置在指定 DOM 范围内自动点击目标元素。
|
||||||
|
*/
|
||||||
async function autoClick(config: PlatformFieldConfig, rootDom: ParentNode): Promise<void> {
|
async function autoClick(config: PlatformFieldConfig, rootDom: ParentNode): Promise<void> {
|
||||||
if (!config.condition) {
|
if (!config.condition) {
|
||||||
return;
|
return;
|
||||||
@@ -62,7 +66,9 @@ async function autoClick(config: PlatformFieldConfig, rootDom: ParentNode): Prom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 递归处理字段配置,支持普通字段、嵌套 row、列表和表格。 */
|
/**
|
||||||
|
* 递归处理字段配置,支持普通字段、嵌套 row、列表和表格。
|
||||||
|
*/
|
||||||
async function processFields(columns: PlatformFieldConfig[], rootDom: ParentNode): Promise<DomScrapeResult> {
|
async function processFields(columns: PlatformFieldConfig[], rootDom: ParentNode): Promise<DomScrapeResult> {
|
||||||
const result: DomScrapeResult = {};
|
const result: DomScrapeResult = {};
|
||||||
|
|
||||||
@@ -100,7 +106,9 @@ async function processFields(columns: PlatformFieldConfig[], rootDom: ParentNode
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按列表配置抓取所有列表项,并按分页配置继续翻页。 */
|
/**
|
||||||
|
* 按列表配置抓取所有列表项,并按分页配置继续翻页。
|
||||||
|
*/
|
||||||
async function processList(config: PlatformFieldConfig, rootDom: ParentNode): Promise<DomScrapeResult[]> {
|
async function processList(config: PlatformFieldConfig, rootDom: ParentNode): Promise<DomScrapeResult[]> {
|
||||||
const allList: DomScrapeResult[] = [];
|
const allList: DomScrapeResult[] = [];
|
||||||
let pageCount = 0;
|
let pageCount = 0;
|
||||||
@@ -148,7 +156,9 @@ async function processList(config: PlatformFieldConfig, rootDom: ParentNode): Pr
|
|||||||
return allList;
|
return allList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按表格配置抓取表格行数据,并按分页配置继续翻页。 */
|
/**
|
||||||
|
* 按表格配置抓取表格行数据,并按分页配置继续翻页。
|
||||||
|
*/
|
||||||
async function processTable(config: PlatformFieldConfig, rootDom: ParentNode): Promise<DomScrapeResult[]> {
|
async function processTable(config: PlatformFieldConfig, rootDom: ParentNode): Promise<DomScrapeResult[]> {
|
||||||
const allTableData: DomScrapeResult[] = [];
|
const allTableData: DomScrapeResult[] = [];
|
||||||
let pageCount = 0;
|
let pageCount = 0;
|
||||||
@@ -221,4 +231,4 @@ async function processTable(config: PlatformFieldConfig, rootDom: ParentNode): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
return allTableData;
|
return allTableData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { handleBackgroundCommand, handleInstalled, handleStartup, handleWindowRemoved } from './service';
|
import { handleBackgroundCommand, handleInstalled, handleStartup, handleWindowRemoved } from './service';
|
||||||
import type { BackgroundCommand } from './types';
|
import type { BackgroundCommand } from './types';
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
@@ -18,7 +18,9 @@ chrome.windows.onRemoved.addListener((windowId) => {
|
|||||||
void handleWindowRemoved(windowId);
|
void handleWindowRemoved(windowId);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 统一包装后台消息处理,确保异步错误能回给调用方。 */
|
/**
|
||||||
|
* 统一包装后台消息处理,确保异步错误能回给调用方。
|
||||||
|
*/
|
||||||
async function handleBackgroundMessage(
|
async function handleBackgroundMessage(
|
||||||
message: BackgroundCommand,
|
message: BackgroundCommand,
|
||||||
sendResponse: (response?: unknown) => void,
|
sendResponse: (response?: unknown) => void,
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import { getPlatformById } from '@/config/platforms';
|
import { getPlatformById } from '@/config/platforms';
|
||||||
import type { CrawlProgressStep, CrawlTaskState, PlatformConfig, PlatformStepConfig } from '@/types';
|
import type { CrawlProgressStep, CrawlTaskState, PlatformConfig, PlatformStepConfig } from '@/types';
|
||||||
import { scrapeDomFields, type DomScrapeResult } from './domScraper';
|
import { scrapeDomFields, type DomScrapeResult } from './domScraper';
|
||||||
import type { BackgroundCommand, BackgroundResponse, CrawlStateResponse } from './types';
|
import type { BackgroundCommand, BackgroundResponse, CrawlStateResponse } from './types';
|
||||||
|
|
||||||
/** chrome.storage.local 中保存当前爬取任务状态的键名。 */
|
// chrome.storage.local 中保存当前爬取任务状态的键名。
|
||||||
const CRAWL_TASK_STORAGE_KEY = 'crawlTaskState';
|
const CRAWL_TASK_STORAGE_KEY = 'crawlTaskState';
|
||||||
|
|
||||||
/** 扩展安装完成时的初始化入口,当前仅保留日志方便调试生命周期。 */
|
/**
|
||||||
|
* 扩展安装完成时的初始化入口,当前仅保留日志方便调试生命周期。
|
||||||
|
*/
|
||||||
export async function handleInstalled(): Promise<void> {
|
export async function handleInstalled(): Promise<void> {
|
||||||
console.log('[background] installed');
|
console.log('[background] installed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 浏览器启动并加载扩展时的初始化入口,当前仅保留日志方便调试生命周期。 */
|
/**
|
||||||
|
* 浏览器启动并加载扩展时的初始化入口,当前仅保留日志方便调试生命周期。
|
||||||
|
*/
|
||||||
export async function handleStartup(): Promise<void> {
|
export async function handleStartup(): Promise<void> {
|
||||||
console.log('[background] startup');
|
console.log('[background] startup');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 监听窗口关闭事件;如果关闭的是爬取窗口,就把当前任务标记为取消。 */
|
/**
|
||||||
|
* 监听窗口关闭事件;如果关闭的是爬取窗口,就把当前任务标记为取消。
|
||||||
|
*/
|
||||||
export async function handleWindowRemoved(windowId: number): Promise<void> {
|
export async function handleWindowRemoved(windowId: number): Promise<void> {
|
||||||
console.log('[background] window removed', windowId);
|
console.log('[background] window removed', windowId);
|
||||||
|
|
||||||
/** 当前保存的爬取任务状态。 */
|
// 当前保存的爬取任务状态。
|
||||||
const state = await getCrawlTaskState();
|
const state = await getCrawlTaskState();
|
||||||
|
|
||||||
if (state?.windowId === windowId && state.status === 'running') {
|
if (state?.windowId === windowId && state.status === 'running') {
|
||||||
@@ -34,7 +40,9 @@ export async function handleWindowRemoved(windowId: number): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据 popup/content 发来的 action 分发到对应的后台处理函数。 */
|
/**
|
||||||
|
* 根据 popup/content 发来的 action 分发到对应的后台处理函数。
|
||||||
|
*/
|
||||||
export async function handleBackgroundCommand(
|
export async function handleBackgroundCommand(
|
||||||
message: BackgroundCommand,
|
message: BackgroundCommand,
|
||||||
): Promise<BackgroundResponse | CrawlStateResponse> {
|
): Promise<BackgroundResponse | CrawlStateResponse> {
|
||||||
@@ -50,9 +58,11 @@ export async function handleBackgroundCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建新的爬取任务,打开目标平台窗口,并把初始时间轴状态写入 storage。 */
|
/**
|
||||||
|
* 创建新的爬取任务,打开目标平台窗口,并把初始时间轴状态写入 storage。
|
||||||
|
*/
|
||||||
async function startCrawl(platformId: string): Promise<CrawlStateResponse> {
|
async function startCrawl(platformId: string): Promise<CrawlStateResponse> {
|
||||||
/** 根据平台 ID 找到对应的平台爬取配置。 */
|
// 根据平台 ID 找到对应的平台爬取配置。
|
||||||
const platform = getPlatformById(platformId);
|
const platform = getPlatformById(platformId);
|
||||||
|
|
||||||
if (!platform) {
|
if (!platform) {
|
||||||
@@ -65,9 +75,10 @@ async function startCrawl(platformId: string): Promise<CrawlStateResponse> {
|
|||||||
return { ok: false, error: '平台未配置爬取步骤' };
|
return { ok: false, error: '平台未配置爬取步骤' };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前任务的开始时间戳,用于计算正计时。 */
|
// 当前任务的开始时间戳,用于计算正计时。
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
/** 窗口创建前的初始任务状态,先写入 storage 让所有页面能立即感知爬取开始。 */
|
// 窗口创建前的初始任务状态。
|
||||||
|
// 先写入 storage 让所有页面能立即感知爬取开始。
|
||||||
const nextState: CrawlTaskState = {
|
const nextState: CrawlTaskState = {
|
||||||
id: `${platform.id}-${startedAt}`,
|
id: `${platform.id}-${startedAt}`,
|
||||||
platformId: platform.id,
|
platformId: platform.id,
|
||||||
@@ -85,15 +96,17 @@ async function startCrawl(platformId: string): Promise<CrawlStateResponse> {
|
|||||||
await setCrawlTaskState(nextState);
|
await setCrawlTaskState(nextState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/** background 创建出来的目标平台窗口信息。 */
|
// background 创建出来的目标平台窗口信息。
|
||||||
const windowInfo = await createCrawlWindow(firstStep.url);
|
const windowInfo = await createCrawlWindow(firstStep.url);
|
||||||
/** 补充 windowId 后的任务状态,后续可用于取消或监听窗口关闭。 */
|
// 补充 windowId 后的任务状态。
|
||||||
|
// 后续可用于取消或监听窗口关闭。
|
||||||
const stateWithWindow = { ...nextState, windowId: windowInfo.id };
|
const stateWithWindow = { ...nextState, windowId: windowInfo.id };
|
||||||
await setCrawlTaskState(stateWithWindow);
|
await setCrawlTaskState(stateWithWindow);
|
||||||
void runCrawlSteps(platform, stateWithWindow);
|
void runCrawlSteps(platform, stateWithWindow);
|
||||||
return { ok: true, data: stateWithWindow };
|
return { ok: true, data: stateWithWindow };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
/** 窗口创建失败时写入的失败状态,供 popup/content 显示错误进度。 */
|
// 窗口创建失败时写入的失败状态。
|
||||||
|
// 供 popup/content 显示错误进度。
|
||||||
const failedState: CrawlTaskState = {
|
const failedState: CrawlTaskState = {
|
||||||
...nextState,
|
...nextState,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
@@ -106,18 +119,21 @@ async function startCrawl(platformId: string): Promise<CrawlStateResponse> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按平台 steps 顺序执行页面跳转、DOM 等待、字段抓取和进度更新。 */
|
/**
|
||||||
|
* 按平台 steps 顺序执行页面跳转、DOM 等待、字段抓取和进度更新。
|
||||||
|
*/
|
||||||
async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskState): Promise<void> {
|
async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskState): Promise<void> {
|
||||||
if (!initialState.windowId) {
|
if (!initialState.windowId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/** 新窗口中的目标标签页 ID,后续所有跳转和脚本注入都依赖它。 */
|
// 新窗口中的目标标签页 ID。
|
||||||
|
// 后续所有跳转和脚本注入都依赖它。
|
||||||
const tabId = await getWindowActiveTabId(initialState.windowId);
|
const tabId = await getWindowActiveTabId(initialState.windowId);
|
||||||
|
|
||||||
for (let stepIndex = 0; stepIndex < platform.steps.length; stepIndex += 1) {
|
for (let stepIndex = 0; stepIndex < platform.steps.length; stepIndex += 1) {
|
||||||
/** 当前正在执行的平台页面步骤配置。 */
|
// 当前正在执行的平台页面步骤配置。
|
||||||
const step = platform.steps[stepIndex];
|
const step = platform.steps[stepIndex];
|
||||||
|
|
||||||
if (!(await isTaskRunning(initialState.id))) {
|
if (!(await isTaskRunning(initialState.id))) {
|
||||||
@@ -127,7 +143,7 @@ async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskSt
|
|||||||
await markStepRunning(initialState.id, stepIndex);
|
await markStepRunning(initialState.id, stepIndex);
|
||||||
await openStepPage(tabId, step.url);
|
await openStepPage(tabId, step.url);
|
||||||
|
|
||||||
/** 当前页面核心 DOM 是否已经出现。 */
|
// 当前页面核心 DOM 是否已经出现。
|
||||||
const isReady = await waitForStepReady(tabId, step);
|
const isReady = await waitForStepReady(tabId, step);
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
@@ -136,7 +152,7 @@ async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskSt
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注入页面执行后的字段抓取结果。 */
|
// 注入页面执行后的字段抓取结果。
|
||||||
const data = await scrapeStepFields(tabId, step);
|
const data = await scrapeStepFields(tabId, step);
|
||||||
console.log(`[crawl] ${platform.name} - ${step.name} 提取成功`, data);
|
console.log(`[crawl] ${platform.name} - ${step.name} 提取成功`, data);
|
||||||
await markStepSuccess(initialState.id, stepIndex);
|
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<number> {
|
async function getWindowActiveTabId(windowId: number): Promise<number> {
|
||||||
/** 指定窗口中查询到的标签页列表。 */
|
// 指定窗口中查询到的标签页列表。
|
||||||
const tabs = await chrome.tabs.query({ windowId, active: true });
|
const tabs = await chrome.tabs.query({ windowId, active: true });
|
||||||
/** 当前窗口里用于承载爬取页面的活动标签页。 */
|
// 当前窗口里用于承载爬取页面的活动标签页。
|
||||||
const tab = tabs[0];
|
const tab = tabs[0];
|
||||||
|
|
||||||
if (!tab?.id) {
|
if (!tab?.id) {
|
||||||
@@ -163,22 +181,28 @@ async function getWindowActiveTabId(windowId: number): Promise<number> {
|
|||||||
return tab.id;
|
return tab.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开某个 steps 页面,并等待浏览器报告 tab 加载完成。 */
|
/**
|
||||||
|
* 打开某个 steps 页面,并等待浏览器报告 tab 加载完成。
|
||||||
|
*/
|
||||||
async function openStepPage(tabId: number, url: string): Promise<void> {
|
async function openStepPage(tabId: number, url: string): Promise<void> {
|
||||||
await chrome.tabs.update(tabId, { url, active: true });
|
await chrome.tabs.update(tabId, { url, active: true });
|
||||||
await waitForTabLoaded(tabId);
|
await waitForTabLoaded(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 等待 tab 完成页面加载。 */
|
/**
|
||||||
|
* 等待 tab 完成页面加载。
|
||||||
|
*/
|
||||||
function waitForTabLoaded(tabId: number): Promise<void> {
|
function waitForTabLoaded(tabId: number): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
/** 页面加载兜底定时器,避免某些站点不触发 complete 时流程永久挂起。 */
|
// 页面加载兜底定时器。
|
||||||
|
// 避免某些站点不触发 complete 时流程永久挂起。
|
||||||
const timeout = globalThis.setTimeout(() => {
|
const timeout = globalThis.setTimeout(() => {
|
||||||
chrome.tabs.onUpdated.removeListener(handleUpdated);
|
chrome.tabs.onUpdated.removeListener(handleUpdated);
|
||||||
resolve();
|
resolve();
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
/** chrome.tabs.onUpdated 的监听器,用于捕获指定 tab 的 complete 状态。 */
|
// chrome.tabs.onUpdated 的监听器。
|
||||||
|
// 用于捕获指定 tab 的 complete 状态。
|
||||||
function handleUpdated(updatedTabId: number, changeInfo: { status?: string }) {
|
function handleUpdated(updatedTabId: number, changeInfo: { status?: string }) {
|
||||||
if (updatedTabId === tabId && changeInfo.status === 'complete') {
|
if (updatedTabId === tabId && changeInfo.status === 'complete') {
|
||||||
globalThis.clearTimeout(timeout);
|
globalThis.clearTimeout(timeout);
|
||||||
@@ -191,7 +215,9 @@ function waitForTabLoaded(tabId: number): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 等待步骤配置中的 checkSelector 出现;第一次超时后刷新页面再重试一次。 */
|
/**
|
||||||
|
* 等待步骤配置中的 checkSelector 出现;第一次超时后刷新页面再重试一次。
|
||||||
|
*/
|
||||||
async function waitForStepReady(tabId: number, step: PlatformStepConfig): Promise<boolean> {
|
async function waitForStepReady(tabId: number, step: PlatformStepConfig): Promise<boolean> {
|
||||||
if (await waitForSelector(tabId, step.checkSelector, 5000)) {
|
if (await waitForSelector(tabId, step.checkSelector, 5000)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -202,13 +228,15 @@ async function waitForStepReady(tabId: number, step: PlatformStepConfig): Promis
|
|||||||
return waitForSelector(tabId, step.checkSelector, 5000);
|
return waitForSelector(tabId, step.checkSelector, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 在目标页面轮询检查指定 selector 是否存在。 */
|
/**
|
||||||
|
* 在目标页面轮询检查指定 selector 是否存在。
|
||||||
|
*/
|
||||||
async function waitForSelector(tabId: number, selector: string, timeoutMs: number): Promise<boolean> {
|
async function waitForSelector(tabId: number, selector: string, timeoutMs: number): Promise<boolean> {
|
||||||
/** 轮询开始时间,用于控制最大等待时长。 */
|
// 轮询开始时间,用于控制最大等待时长。
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
|
||||||
while (Date.now() - startedAt < timeoutMs) {
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
/** 当前页面是否已经能查询到目标元素。 */
|
// 当前页面是否已经能查询到目标元素。
|
||||||
const exists = await checkSelectorExists(tabId, selector);
|
const exists = await checkSelectorExists(tabId, selector);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
@@ -221,9 +249,11 @@ async function waitForSelector(tabId: number, selector: string, timeoutMs: numbe
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注入轻量脚本检查页面里是否存在指定 selector。 */
|
/**
|
||||||
|
* 注入轻量脚本检查页面里是否存在指定 selector。
|
||||||
|
*/
|
||||||
async function checkSelectorExists(tabId: number, selector: string): Promise<boolean> {
|
async function checkSelectorExists(tabId: number, selector: string): Promise<boolean> {
|
||||||
/** chrome.scripting.executeScript 返回的注入执行结果。 */
|
// chrome.scripting.executeScript 返回的注入执行结果。
|
||||||
const results = await chrome.scripting.executeScript({
|
const results = await chrome.scripting.executeScript({
|
||||||
target: { tabId },
|
target: { tabId },
|
||||||
func: (targetSelector: string) => Boolean(document.querySelector(targetSelector)),
|
func: (targetSelector: string) => Boolean(document.querySelector(targetSelector)),
|
||||||
@@ -233,9 +263,11 @@ async function checkSelectorExists(tabId: number, selector: string): Promise<boo
|
|||||||
return Boolean(results[0]?.result);
|
return Boolean(results[0]?.result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注入 domScraper 到目标页面,并根据当前 step.fields 提取页面数据。 */
|
/**
|
||||||
|
* 注入 domScraper 到目标页面,并根据当前 step.fields 提取页面数据。
|
||||||
|
*/
|
||||||
async function scrapeStepFields(tabId: number, step: PlatformStepConfig): Promise<DomScrapeResult | null> {
|
async function scrapeStepFields(tabId: number, step: PlatformStepConfig): Promise<DomScrapeResult | null> {
|
||||||
/** 目标页面执行 DOM 抓取后返回的结果数组。 */
|
// 目标页面执行 DOM 抓取后返回的结果数组。
|
||||||
const results = await chrome.scripting.executeScript({
|
const results = await chrome.scripting.executeScript({
|
||||||
target: { tabId },
|
target: { tabId },
|
||||||
func: scrapeDomFields,
|
func: scrapeDomFields,
|
||||||
@@ -245,14 +277,18 @@ async function scrapeStepFields(tabId: number, step: PlatformStepConfig): Promis
|
|||||||
return results[0]?.result ?? null;
|
return results[0]?.result ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断指定任务是否仍处于 running 状态。 */
|
/**
|
||||||
|
* 判断指定任务是否仍处于 running 状态。
|
||||||
|
*/
|
||||||
async function isTaskRunning(taskId: string): Promise<boolean> {
|
async function isTaskRunning(taskId: string): Promise<boolean> {
|
||||||
/** 当前 storage 中的任务状态。 */
|
// 当前 storage 中的任务状态。
|
||||||
const state = await getCrawlTaskState();
|
const state = await getCrawlTaskState();
|
||||||
return state?.id === taskId && state.status === 'running';
|
return state?.id === taskId && state.status === 'running';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将指定步骤标记为运行中,同时把其它未完成步骤保持为等待。 */
|
/**
|
||||||
|
* 将指定步骤标记为运行中,同时把其它未完成步骤保持为等待。
|
||||||
|
*/
|
||||||
async function markStepRunning(taskId: string, stepIndex: number): Promise<void> {
|
async function markStepRunning(taskId: string, stepIndex: number): Promise<void> {
|
||||||
await updateCrawlTaskState(taskId, (state) => ({
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -266,7 +302,9 @@ async function markStepRunning(taskId: string, stepIndex: number): Promise<void>
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将指定步骤标记为成功。 */
|
/**
|
||||||
|
* 将指定步骤标记为成功。
|
||||||
|
*/
|
||||||
async function markStepSuccess(taskId: string, stepIndex: number): Promise<void> {
|
async function markStepSuccess(taskId: string, stepIndex: number): Promise<void> {
|
||||||
await updateCrawlTaskState(taskId, (state) => ({
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -276,7 +314,9 @@ async function markStepSuccess(taskId: string, stepIndex: number): Promise<void>
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将指定步骤标记为失败,并记录失败原因。 */
|
/**
|
||||||
|
* 将指定步骤标记为失败,并记录失败原因。
|
||||||
|
*/
|
||||||
async function markStepFailed(taskId: string, stepIndex: number, message: string): Promise<void> {
|
async function markStepFailed(taskId: string, stepIndex: number, message: string): Promise<void> {
|
||||||
await updateCrawlTaskState(taskId, (state) => ({
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -287,7 +327,9 @@ async function markStepFailed(taskId: string, stepIndex: number, message: string
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将整个任务标记为完成。 */
|
/**
|
||||||
|
* 将整个任务标记为完成。
|
||||||
|
*/
|
||||||
async function markTaskCompleted(taskId: string): Promise<void> {
|
async function markTaskCompleted(taskId: string): Promise<void> {
|
||||||
await updateCrawlTaskState(taskId, (state) => ({
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -296,7 +338,9 @@ async function markTaskCompleted(taskId: string): Promise<void> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将整个任务标记为失败。 */
|
/**
|
||||||
|
* 将整个任务标记为失败。
|
||||||
|
*/
|
||||||
async function markTaskFailed(taskId: string, message = '爬取失败'): Promise<void> {
|
async function markTaskFailed(taskId: string, message = '爬取失败'): Promise<void> {
|
||||||
await updateCrawlTaskState(taskId, (state) => ({
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -307,12 +351,14 @@ async function markTaskFailed(taskId: string, message = '爬取失败'): Promise
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 读取任务状态后执行不可变更新,避免覆盖已取消或已替换的任务。 */
|
/**
|
||||||
|
* 读取任务状态后执行不可变更新,避免覆盖已取消或已替换的任务。
|
||||||
|
*/
|
||||||
async function updateCrawlTaskState(
|
async function updateCrawlTaskState(
|
||||||
taskId: string,
|
taskId: string,
|
||||||
updater: (state: CrawlTaskState) => CrawlTaskState,
|
updater: (state: CrawlTaskState) => CrawlTaskState,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
/** 当前 storage 中最新的任务状态。 */
|
// 当前 storage 中最新的任务状态。
|
||||||
const state = await getCrawlTaskState();
|
const state = await getCrawlTaskState();
|
||||||
|
|
||||||
if (!state || state.id !== taskId || state.status === 'canceled') {
|
if (!state || state.id !== taskId || state.status === 'canceled') {
|
||||||
@@ -322,23 +368,28 @@ async function updateCrawlTaskState(
|
|||||||
await setCrawlTaskState(updater(state));
|
await setCrawlTaskState(updater(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 睡眠工具,用于轮询 DOM 等待。 */
|
/**
|
||||||
|
* 睡眠工具,用于轮询 DOM 等待。
|
||||||
|
*/
|
||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
globalThis.setTimeout(resolve, ms);
|
globalThis.setTimeout(resolve, ms);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取消当前爬取任务,并尝试关闭正在爬取的平台窗口。 */
|
/**
|
||||||
|
* 取消当前爬取任务,并尝试关闭正在爬取的平台窗口。
|
||||||
|
*/
|
||||||
async function cancelCrawl(): Promise<CrawlStateResponse> {
|
async function cancelCrawl(): Promise<CrawlStateResponse> {
|
||||||
/** 当前保存的爬取任务状态。 */
|
// 当前保存的爬取任务状态。
|
||||||
const state = await getCrawlTaskState();
|
const state = await getCrawlTaskState();
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return { ok: true, data: null };
|
return { ok: true, data: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 用户取消后的任务状态,当前执行步骤会显示为失败并附带取消原因。 */
|
// 用户取消后的任务状态。
|
||||||
|
// 当前执行步骤会显示为失败并附带取消原因。
|
||||||
const canceledState: CrawlTaskState = {
|
const canceledState: CrawlTaskState = {
|
||||||
...state,
|
...state,
|
||||||
status: 'canceled',
|
status: 'canceled',
|
||||||
@@ -356,21 +407,28 @@ async function cancelCrawl(): Promise<CrawlStateResponse> {
|
|||||||
return { ok: true, data: canceledState };
|
return { ok: true, data: canceledState };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从 chrome.storage.local 读取当前爬取任务状态。 */
|
/**
|
||||||
|
* 从 chrome.storage.local 读取当前爬取任务状态。
|
||||||
|
*/
|
||||||
async function getCrawlTaskState(): Promise<CrawlTaskState | null> {
|
async function getCrawlTaskState(): Promise<CrawlTaskState | null> {
|
||||||
/** chrome.storage.local 返回的原始键值对象。 */
|
// chrome.storage.local 返回的原始键值对象。
|
||||||
const result = await chrome.storage.local.get(CRAWL_TASK_STORAGE_KEY);
|
const result = await chrome.storage.local.get(CRAWL_TASK_STORAGE_KEY);
|
||||||
/** 取出的任务状态候选值,需要经过结构校验后才能使用。 */
|
// 取出的任务状态候选值。
|
||||||
|
// 需要经过结构校验后才能使用。
|
||||||
const state = result[CRAWL_TASK_STORAGE_KEY];
|
const state = result[CRAWL_TASK_STORAGE_KEY];
|
||||||
return isCrawlTaskState(state) ? state : null;
|
return isCrawlTaskState(state) ? state : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将最新爬取任务状态写入 chrome.storage.local,供 popup 和 content script 同步读取。 */
|
/**
|
||||||
|
* 将最新爬取任务状态写入 chrome.storage.local,供 popup 和 content script 同步读取。
|
||||||
|
*/
|
||||||
async function setCrawlTaskState(state: CrawlTaskState): Promise<void> {
|
async function setCrawlTaskState(state: CrawlTaskState): Promise<void> {
|
||||||
await chrome.storage.local.set({ [CRAWL_TASK_STORAGE_KEY]: state });
|
await chrome.storage.local.set({ [CRAWL_TASK_STORAGE_KEY]: state });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开一个普通浏览器窗口承载目标平台页面。 */
|
/**
|
||||||
|
* 打开一个普通浏览器窗口承载目标平台页面。
|
||||||
|
*/
|
||||||
function createCrawlWindow(url: string): Promise<chrome.windows.Window> {
|
function createCrawlWindow(url: string): Promise<chrome.windows.Window> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
chrome.windows.create(
|
chrome.windows.create(
|
||||||
@@ -382,7 +440,7 @@ function createCrawlWindow(url: string): Promise<chrome.windows.Window> {
|
|||||||
height: 900,
|
height: 900,
|
||||||
},
|
},
|
||||||
(windowInfo) => {
|
(windowInfo) => {
|
||||||
/** Chrome 扩展 API 回调中的运行时错误。 */
|
// Chrome 扩展 API 回调中的运行时错误。
|
||||||
const runtimeError = chrome.runtime.lastError;
|
const runtimeError = chrome.runtime.lastError;
|
||||||
|
|
||||||
if (runtimeError) {
|
if (runtimeError) {
|
||||||
@@ -401,7 +459,9 @@ function createCrawlWindow(url: string): Promise<chrome.windows.Window> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据窗口 ID 关闭爬取窗口;关闭失败时不阻塞取消状态写入。 */
|
/**
|
||||||
|
* 根据窗口 ID 关闭爬取窗口;关闭失败时不阻塞取消状态写入。
|
||||||
|
*/
|
||||||
function removeWindow(windowId: number): Promise<void> {
|
function removeWindow(windowId: number): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
chrome.windows.remove(windowId, () => {
|
chrome.windows.remove(windowId, () => {
|
||||||
@@ -410,7 +470,9 @@ function removeWindow(windowId: number): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 粗略判断 storage 中读取到的值是否像一个爬取任务状态对象。 */
|
/**
|
||||||
|
* 粗略判断 storage 中读取到的值是否像一个爬取任务状态对象。
|
||||||
|
*/
|
||||||
function isCrawlTaskState(value: unknown): value is CrawlTaskState {
|
function isCrawlTaskState(value: unknown): value is CrawlTaskState {
|
||||||
return typeof value === 'object' && value !== null && 'id' in value && 'steps' in value;
|
return typeof value === 'object' && value !== null && 'id' in value && 'steps' in value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
import type { CrawlTaskState } from '@/types';
|
import type { CrawlTaskState } from '@/types';
|
||||||
|
|
||||||
/** 启动爬取任务的后台消息。 */
|
// 启动爬取任务的后台消息。
|
||||||
export interface StartCrawlCommand {
|
export interface StartCrawlCommand {
|
||||||
/** 消息动作类型:请求 background 创建爬取窗口并初始化任务状态。 */
|
// 消息动作类型:请求 background 创建爬取窗口并初始化任务状态。
|
||||||
action: 'START_CRAWL';
|
action: 'START_CRAWL';
|
||||||
/** 启动爬取所需参数。 */
|
// 启动爬取所需参数。
|
||||||
payload: {
|
payload: {
|
||||||
/** 当前要爬取的平台 ID,对应 config/platforms.ts 中的平台配置。 */
|
// 当前要爬取的平台 ID,对应 config/platforms.ts 中的平台配置。
|
||||||
platformId: string;
|
platformId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取当前爬取任务状态的后台消息。 */
|
// 获取当前爬取任务状态的后台消息。
|
||||||
export interface GetCrawlStateCommand {
|
export interface GetCrawlStateCommand {
|
||||||
/** 消息动作类型:请求 background 返回当前任务快照。 */
|
// 消息动作类型:请求 background 返回当前任务快照。
|
||||||
action: 'GET_CRAWL_STATE';
|
action: 'GET_CRAWL_STATE';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取消当前爬取任务的后台消息。 */
|
// 取消当前爬取任务的后台消息。
|
||||||
export interface CancelCrawlCommand {
|
export interface CancelCrawlCommand {
|
||||||
/** 消息动作类型:请求 background 标记任务取消并关闭爬取窗口。 */
|
// 消息动作类型:请求 background 标记任务取消并关闭爬取窗口。
|
||||||
action: 'CANCEL_CRAWL';
|
action: 'CANCEL_CRAWL';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** popup/content script 能发送给 background 的全部消息类型。 */
|
// popup/content script 能发送给 background 的全部消息类型。
|
||||||
export type BackgroundCommand = StartCrawlCommand | GetCrawlStateCommand | CancelCrawlCommand;
|
export type BackgroundCommand = StartCrawlCommand | GetCrawlStateCommand | CancelCrawlCommand;
|
||||||
|
|
||||||
/** background 统一响应结构。 */
|
// background 统一响应结构。
|
||||||
export interface BackgroundResponse<T = unknown> {
|
export interface BackgroundResponse<T = unknown> {
|
||||||
/** 当前请求是否处理成功。 */
|
// 当前请求是否处理成功。
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
/** 成功或部分失败时返回的业务数据。 */
|
// 成功或部分失败时返回的业务数据。
|
||||||
data?: T;
|
data?: T;
|
||||||
/** 请求失败时返回的错误文案。 */
|
// 请求失败时返回的错误文案。
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取或变更爬取任务后返回的响应结构。 */
|
// 获取或变更爬取任务后返回的响应结构。
|
||||||
export type CrawlStateResponse = BackgroundResponse<CrawlTaskState | null>;
|
export type CrawlStateResponse = BackgroundResponse<CrawlTaskState | null>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
import type { PlatformConfig } from '@/types';
|
import type { PlatformConfig } from '@/types';
|
||||||
|
|
||||||
export const PLATFORM_CONFIGS: PlatformConfig[] = [
|
export const PLATFORM_CONFIGS: PlatformConfig[] = [
|
||||||
@@ -123,7 +123,9 @@ export const PLATFORM_CONFIGS: PlatformConfig[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 根据平台 ID 返回对应的平台抓取配置。 */
|
/**
|
||||||
|
* 根据平台 ID 返回对应的平台抓取配置。
|
||||||
|
*/
|
||||||
export function getPlatformById(platformId: string) {
|
export function getPlatformById(platformId: string) {
|
||||||
return PLATFORM_CONFIGS.find((item) => item.id === platformId) ?? null;
|
return PLATFORM_CONFIGS.find((item) => item.id === platformId) ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
||||||
import type {CrawlTaskState} from '@/types';
|
import type {CrawlTaskState} from '@/types';
|
||||||
|
|
||||||
/** 当前后台保存的爬取任务快照,用于决定是否展示右下角浮窗。 */
|
// 当前后台保存的爬取任务快照,用于决定是否展示右下角浮窗。
|
||||||
const crawlState = ref<CrawlTaskState | null>(null);
|
const crawlState = ref<CrawlTaskState | null>(null);
|
||||||
/** 当前爬取任务已经运行的秒数,页面上会格式化为 mm:ss。 */
|
// 当前爬取任务已经运行的秒数,页面上会格式化为 mm:ss。
|
||||||
const elapsedSeconds = ref(0);
|
const elapsedSeconds = ref(0);
|
||||||
/** 控制右下角时间轴面板是否展开。 */
|
// 控制右下角时间轴面板是否展开。
|
||||||
const isPanelOpen = ref(false);
|
const isPanelOpen = ref(false);
|
||||||
/** 轮询后台爬取状态和刷新计时器的定时器 ID。 */
|
// 轮询后台爬取状态和刷新计时器的定时器 ID。
|
||||||
let timer: number | undefined;
|
let timer: number | undefined;
|
||||||
|
|
||||||
/** 只有任务处于运行中时,才在网页右下角展示计时按钮。 */
|
// 只有任务处于运行中时,才在网页右下角展示计时按钮。
|
||||||
const isVisible = computed(() => crawlState.value?.status === 'running');
|
const isVisible = computed(() => crawlState.value?.status === 'running');
|
||||||
|
|
||||||
/** 内容脚本挂载后立即同步一次状态,并开始每秒刷新计时和任务进度。 */
|
// 内容脚本挂载后立即同步一次状态,并开始每秒刷新计时和任务进度。
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void refreshCrawlState();
|
void refreshCrawlState();
|
||||||
timer = window.setInterval(() => {
|
timer = window.setInterval(() => {
|
||||||
@@ -23,16 +23,18 @@ onMounted(() => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 内容脚本卸载时清理定时器,避免页面残留轮询。 */
|
// 内容脚本卸载时清理定时器,避免页面残留轮询。
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
window.clearInterval(timer);
|
window.clearInterval(timer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 从 background 获取最新爬取任务状态,并在任务结束时自动收起面板。 */
|
/**
|
||||||
|
* 从 background 获取最新爬取任务状态,并在任务结束时自动收起面板。
|
||||||
|
*/
|
||||||
async function refreshCrawlState() {
|
async function refreshCrawlState() {
|
||||||
/** background 返回的当前爬取任务状态响应。 */
|
// background 返回的当前爬取任务状态响应。
|
||||||
const response = await sendBackgroundMessage<CrawlTaskState | null>({action: 'GET_CRAWL_STATE'});
|
const response = await sendBackgroundMessage<CrawlTaskState | null>({action: 'GET_CRAWL_STATE'});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -45,7 +47,9 @@ async function refreshCrawlState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据任务开始时间实时计算已经运行的秒数。 */
|
/**
|
||||||
|
* 根据任务开始时间实时计算已经运行的秒数。
|
||||||
|
*/
|
||||||
function updateElapsedSeconds() {
|
function updateElapsedSeconds() {
|
||||||
if (!crawlState.value) {
|
if (!crawlState.value) {
|
||||||
elapsedSeconds.value = 0;
|
elapsedSeconds.value = 0;
|
||||||
@@ -55,18 +59,22 @@ function updateElapsedSeconds() {
|
|||||||
elapsedSeconds.value = Math.max(0, Math.floor((Date.now() - crawlState.value.startedAt) / 1000));
|
elapsedSeconds.value = Math.max(0, Math.floor((Date.now() - crawlState.value.startedAt) / 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将秒数格式化为 mm:ss,展示在圆形计时按钮和面板标题里。 */
|
/**
|
||||||
|
* 将秒数格式化为 mm:ss,展示在圆形计时按钮和面板标题里。
|
||||||
|
*/
|
||||||
function formatElapsed(totalSeconds: number): string {
|
function formatElapsed(totalSeconds: number): string {
|
||||||
/** 运行时长中的分钟部分。 */
|
// 运行时长中的分钟部分。
|
||||||
const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
|
const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
|
||||||
/** 运行时长中的秒数部分。 */
|
// 运行时长中的秒数部分。
|
||||||
const seconds = (totalSeconds % 60).toString().padStart(2, '0');
|
const seconds = (totalSeconds % 60).toString().padStart(2, '0');
|
||||||
return `${minutes}:${seconds}`;
|
return `${minutes}:${seconds}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将步骤状态枚举转换成中文展示文案。 */
|
/**
|
||||||
|
* 将步骤状态枚举转换成中文展示文案。
|
||||||
|
*/
|
||||||
function getStepText(status: string): string {
|
function getStepText(status: string): string {
|
||||||
/** 步骤状态到展示文案的映射表。 */
|
// 步骤状态到展示文案的映射表。
|
||||||
const textMap: Record<string, string> = {
|
const textMap: Record<string, string> = {
|
||||||
pending: '等待中',
|
pending: '等待中',
|
||||||
running: '爬取中',
|
running: '爬取中',
|
||||||
@@ -77,7 +85,9 @@ function getStepText(status: string): string {
|
|||||||
return textMap[status] ?? status;
|
return textMap[status] ?? status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发送消息到 background;非扩展环境下返回空成功响应,方便本地页面不报错。 */
|
/**
|
||||||
|
* 发送消息到 background;非扩展环境下返回空成功响应,方便本地页面不报错。
|
||||||
|
*/
|
||||||
function sendBackgroundMessage<T>(message: unknown): Promise<{ ok: boolean; data?: T; error?: string }> {
|
function sendBackgroundMessage<T>(message: unknown): Promise<{ ok: boolean; data?: T; error?: string }> {
|
||||||
if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
|
if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
|
||||||
return Promise.resolve({ok: true, data: null as T});
|
return Promise.resolve({ok: true, data: null as T});
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
|
||||||
/** 将内容脚本应用挂载到页面中。 */
|
/**
|
||||||
|
* 将内容脚本应用挂载到页面中。
|
||||||
|
*/
|
||||||
function mountApp() {
|
function mountApp() {
|
||||||
if (document.getElementById('dianshan-crx-root')) {
|
if (document.getElementById('dianshan-crx-root')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 内容脚本在宿主页面中的根容器,用于避免污染业务页面结构。 */
|
// 内容脚本在宿主页面中的根容器。
|
||||||
|
// 用于避免污染业务页面结构。
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.id = 'dianshan-crx-root';
|
container.id = 'dianshan-crx-root';
|
||||||
/** Vue 应用实际挂载的节点。 */
|
// Vue 应用实际挂载的节点。
|
||||||
const appRoot = document.createElement('div');
|
const appRoot = document.createElement('div');
|
||||||
|
|
||||||
container.appendChild(appRoot);
|
container.appendChild(appRoot);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
const AUTH_TOKEN_KEY = 'token';
|
const AUTH_TOKEN_KEY = 'token';
|
||||||
const MOCK_TOKEN = 'mock-extension-token';
|
const MOCK_TOKEN = 'mock-extension-token';
|
||||||
|
|
||||||
/** 获取当前登录 token。 */
|
/**
|
||||||
|
* 获取当前登录 token。
|
||||||
|
*/
|
||||||
export async function getToken(): Promise<string | null> {
|
export async function getToken(): Promise<string | null> {
|
||||||
const storage = getChromeStorage();
|
const storage = getChromeStorage();
|
||||||
|
|
||||||
@@ -14,13 +16,17 @@ export async function getToken(): Promise<string | null> {
|
|||||||
return window.localStorage.getItem(AUTH_TOKEN_KEY);
|
return window.localStorage.getItem(AUTH_TOKEN_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模拟登录,写入一个临时 token,方便后续替换真实登录逻辑。 */
|
/**
|
||||||
|
* 模拟登录,写入一个临时 token,方便后续替换真实登录逻辑。
|
||||||
|
*/
|
||||||
export async function mockLogin(): Promise<string> {
|
export async function mockLogin(): Promise<string> {
|
||||||
await setToken(MOCK_TOKEN);
|
await setToken(MOCK_TOKEN);
|
||||||
return MOCK_TOKEN;
|
return MOCK_TOKEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清除当前登录 token。 */
|
/**
|
||||||
|
* 清除当前登录 token。
|
||||||
|
*/
|
||||||
export async function logout(): Promise<void> {
|
export async function logout(): Promise<void> {
|
||||||
const storage = getChromeStorage();
|
const storage = getChromeStorage();
|
||||||
|
|
||||||
@@ -44,7 +50,7 @@ async function setToken(token: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取谷歌缓存
|
* * 获取谷歌缓存
|
||||||
*/
|
*/
|
||||||
function getChromeStorage(): chrome.storage.StorageArea | null {
|
function getChromeStorage(): chrome.storage.StorageArea | null {
|
||||||
if (typeof chrome === 'undefined' || !chrome.storage?.local) {
|
if (typeof chrome === 'undefined' || !chrome.storage?.local) {
|
||||||
|
|||||||
@@ -1,45 +1,37 @@
|
|||||||
/**
|
// 单个爬取步骤的执行状态。
|
||||||
* 单个爬取步骤的执行状态。
|
|
||||||
*/
|
|
||||||
export type CrawlStepStatus = 'pending' | 'running' | 'success' | 'failed';
|
export type CrawlStepStatus = 'pending' | 'running' | 'success' | 'failed';
|
||||||
|
|
||||||
/**
|
// 整体爬取任务状态。
|
||||||
* 整体爬取任务状态。
|
|
||||||
*/
|
|
||||||
export type CrawlTaskStatus = 'running' | 'completed' | 'failed' | 'canceled';
|
export type CrawlTaskStatus = 'running' | 'completed' | 'failed' | 'canceled';
|
||||||
|
|
||||||
/**
|
// 时间轴中的单个爬取步骤进度。
|
||||||
* 时间轴中的单个爬取步骤进度。
|
|
||||||
*/
|
|
||||||
export interface CrawlProgressStep {
|
export interface CrawlProgressStep {
|
||||||
/** 步骤名称,用于展示给用户。 */
|
// 步骤名称,用于展示给用户。
|
||||||
name: string;
|
name: string;
|
||||||
/** 步骤唯一标识,对应平台配置 steps 中的 uniqueKey。 */
|
// 步骤唯一标识,对应平台配置 steps 中的 uniqueKey。
|
||||||
uniqueKey: string;
|
uniqueKey: string;
|
||||||
/** 当前步骤执行状态。 */
|
// 当前步骤执行状态。
|
||||||
status: CrawlStepStatus;
|
status: CrawlStepStatus;
|
||||||
/** 状态补充说明,如失败原因。 */
|
// 状态补充说明,如失败原因。
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 当前正在执行的爬取任务快照,供 popup 和 content script 同步展示。
|
||||||
* 当前正在执行的爬取任务快照,供 popup 和 content script 同步展示。
|
|
||||||
*/
|
|
||||||
export interface CrawlTaskState {
|
export interface CrawlTaskState {
|
||||||
/** 任务唯一标识。 */
|
// 任务唯一标识。
|
||||||
id: string;
|
id: string;
|
||||||
/** 当前爬取平台 ID。 */
|
// 当前爬取平台 ID。
|
||||||
platformId: string;
|
platformId: string;
|
||||||
/** 当前爬取平台名称。 */
|
// 当前爬取平台名称。
|
||||||
platformName: string;
|
platformName: string;
|
||||||
/** 爬取窗口 ID,由 background 创建窗口后写入。 */
|
// 爬取窗口 ID,由 background 创建窗口后写入。
|
||||||
windowId?: number;
|
windowId?: number;
|
||||||
/** 任务开始时间戳。 */
|
// 任务开始时间戳。
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
/** 当前任务状态。 */
|
// 当前任务状态。
|
||||||
status: CrawlTaskStatus;
|
status: CrawlTaskStatus;
|
||||||
/** 当前执行到的步骤下标。 */
|
// 当前执行到的步骤下标。
|
||||||
currentStepIndex: number;
|
currentStepIndex: number;
|
||||||
/** 平台 steps 映射出的时间轴进度。 */
|
// 平台 steps 映射出的时间轴进度。
|
||||||
steps: CrawlProgressStep[];
|
steps: CrawlProgressStep[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +1,86 @@
|
|||||||
/**
|
// 字段采集类型:0 普通元素(默认),1 列表,2 表格(带分页)。
|
||||||
* 字段采集类型:0 普通元素(默认),1 列表,2 表格(带分页)。
|
|
||||||
*/
|
|
||||||
export type PlatformFieldType = 0 | 1 | 2;
|
export type PlatformFieldType = 0 | 1 | 2;
|
||||||
|
|
||||||
/**
|
// 条件点击配置,用于进入某个页面或采集某个字段前按顺序点击页面元素。
|
||||||
* 条件点击配置,用于进入某个页面或采集某个字段前按顺序点击页面元素。
|
|
||||||
*/
|
|
||||||
export interface PlatformClickCondition {
|
export interface PlatformClickCondition {
|
||||||
/** 需要点击的元素选择器列表,会按数组顺序依次执行。 */
|
// 需要点击的元素选择器列表,会按数组顺序依次执行。
|
||||||
list: string[];
|
list: string[];
|
||||||
/** 点击后的等待时间,单位毫秒。 */
|
// 点击后的等待时间,单位毫秒。
|
||||||
time: number;
|
time: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 分页配置,用于列表或表格字段存在翻页时控制下一页采集。
|
||||||
* 分页配置,用于列表或表格字段存在翻页时控制下一页采集。
|
|
||||||
*/
|
|
||||||
export interface PlatformPaginationConfig {
|
export interface PlatformPaginationConfig {
|
||||||
/** 下一页按钮的 CSS 选择器。 */
|
// 下一页按钮的 CSS 选择器。
|
||||||
nextBtn: string;
|
nextBtn: string;
|
||||||
/** 最多采集页数,避免无限翻页。 */
|
// 最多采集页数,避免无限翻页。
|
||||||
maxPage?: number;
|
maxPage?: number;
|
||||||
/** 每次翻页后的等待时间,单位毫秒。 */
|
// 每次翻页后的等待时间,单位毫秒。
|
||||||
delay?: number;
|
delay?: number;
|
||||||
/** 下一页按钮不可用时的 class 名称。 */
|
// 下一页按钮不可用时的 class 名称。
|
||||||
disabledClass?: string;
|
disabledClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 表格分段配置,用于兼容一个数据块由多个 table 或多个 table 片段组成的情况。
|
||||||
* 表格分段配置,用于兼容一个数据块由多个 table 或多个 table 片段组成的情况。
|
|
||||||
*/
|
|
||||||
export interface PlatformTablePartConfig {
|
export interface PlatformTablePartConfig {
|
||||||
/** 当前 table 或表格片段的名称。 */
|
// 当前 table 或表格片段的名称。
|
||||||
label: string;
|
label: string;
|
||||||
/** 当前 table 或表格片段的兼容名称,兼容 message.js 中的 name 写法。 */
|
// 当前 table 或表格片段的兼容名称,兼容 message.js 中的 name 写法。
|
||||||
name?: string;
|
name?: string;
|
||||||
/** 当前 table 或表格片段的 CSS 选择器。 */
|
// 当前 table 或表格片段的 CSS 选择器。
|
||||||
className: string;
|
className: string;
|
||||||
/** 当前 table 或表格片段的兼容选择器,兼容 message.js 中的 select 写法。 */
|
// 当前 table 或表格片段的兼容选择器,兼容 message.js 中的 select 写法。
|
||||||
select?: string;
|
select?: string;
|
||||||
/** 行元素选择器,不填时由采集逻辑使用默认行选择器。 */
|
// 行元素选择器,不填时由采集逻辑使用默认行选择器。
|
||||||
rowSelector?: string;
|
rowSelector?: string;
|
||||||
/** 当前 table 或表格片段下需要采集的字段。 */
|
// 当前 table 或表格片段下需要采集的字段。
|
||||||
keys?: PlatformFieldConfig[];
|
keys?: PlatformFieldConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 页面字段配置,描述一个普通元素、列表元素或表格元素如何从 DOM 中提取数据。
|
||||||
* 页面字段配置,描述一个普通元素、列表元素或表格元素如何从 DOM 中提取数据。
|
|
||||||
*/
|
|
||||||
export interface PlatformFieldConfig {
|
export interface PlatformFieldConfig {
|
||||||
/** 字段显示名,也是最终打印数据中的键名。 */
|
// 字段显示名,也是最终打印数据中的键名。
|
||||||
label: string;
|
label: string;
|
||||||
/** 字段对应的 CSS 选择器。 */
|
// 字段对应的 CSS 选择器。
|
||||||
className: string;
|
className: string;
|
||||||
/** 需要提取的属性名;不填时默认提取文本,图片和链接会自动取 src/href。 */
|
// 需要提取的属性名;不填时默认提取文本,图片和链接会自动取 src/href。
|
||||||
attr?: string;
|
attr?: string;
|
||||||
/** 表格字段所属的表格分段名称,用于横向拼接多 table 行数据。 */
|
// 表格字段所属的表格分段名称,用于横向拼接多 table 行数据。
|
||||||
part?: string;
|
part?: string;
|
||||||
/** 字段类型:0 普通元素(默认),1 列表,2 表格。 */
|
// 字段类型:0 普通元素(默认),1 列表,2 表格。
|
||||||
type?: PlatformFieldType;
|
type?: PlatformFieldType;
|
||||||
/** 进入该字段采集前需要执行的点击条件。 */
|
// 进入该字段采集前需要执行的点击条件。
|
||||||
condition?: PlatformClickCondition;
|
condition?: PlatformClickCondition;
|
||||||
/** 子元素字段;普通元素下表示嵌套键值,列表或表格下表示每项/每行的字段。 */
|
// 子元素字段;普通元素下表示嵌套键值,列表或表格下表示每项/每行的字段。
|
||||||
keys?: PlatformFieldConfig[];
|
keys?: PlatformFieldConfig[];
|
||||||
/** 表格专用配置,用于多个 table 或分段 table 的组合采集。 */
|
// 表格专用配置,用于多个 table 或分段 table 的组合采集。
|
||||||
tableParts?: PlatformTablePartConfig[];
|
tableParts?: PlatformTablePartConfig[];
|
||||||
/** 分页配置,常用于列表和表格字段。 */
|
// 分页配置,常用于列表和表格字段。
|
||||||
pagination?: PlatformPaginationConfig;
|
pagination?: PlatformPaginationConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 单个抓取页面步骤配置,描述页面地址、可用性检查和需要采集的字段。
|
||||||
* 单个抓取页面步骤配置,描述页面地址、可用性检查和需要采集的字段。
|
|
||||||
*/
|
|
||||||
export interface PlatformStepConfig {
|
export interface PlatformStepConfig {
|
||||||
/** 步骤显示名,用于进度展示。 */
|
// 步骤显示名,用于进度展示。
|
||||||
name: string;
|
name: string;
|
||||||
/** 步骤唯一标识,用于状态记录和结果归类。 */
|
// 步骤唯一标识,用于状态记录和结果归类。
|
||||||
uniqueKey: string;
|
uniqueKey: string;
|
||||||
/** 当前步骤需要打开或跳转到的页面地址。 */
|
// 当前步骤需要打开或跳转到的页面地址。
|
||||||
url: string;
|
url: string;
|
||||||
/** 判断页面 DOM 是否加载完成的 CSS 选择器。 */
|
// 判断页面 DOM 是否加载完成的 CSS 选择器。
|
||||||
checkSelector: string;
|
checkSelector: string;
|
||||||
/** 当前页面需要采集的字段列表。 */
|
// 当前页面需要采集的字段列表。
|
||||||
fields: PlatformFieldConfig[];
|
fields: PlatformFieldConfig[];
|
||||||
/** 进入该步骤前需要执行的点击条件。 */
|
// 进入该步骤前需要执行的点击条件。
|
||||||
condition?: PlatformClickCondition;
|
condition?: PlatformClickCondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 平台抓取配置,描述一个商家后台平台的入口地址和页面抓取顺序。
|
||||||
* 平台抓取配置,描述一个商家后台平台的入口地址和页面抓取顺序。
|
|
||||||
*/
|
|
||||||
export interface PlatformConfig {
|
export interface PlatformConfig {
|
||||||
/** 平台唯一标识,用于 popup 选择和后台任务定位。 */
|
// 平台唯一标识,用于 popup 选择和后台任务定位。
|
||||||
id: string;
|
id: string;
|
||||||
/** 平台显示名称。 */
|
// 平台显示名称。
|
||||||
name: string;
|
name: string;
|
||||||
/** 当前平台的页面抓取顺序。 */
|
// 当前平台的页面抓取顺序。
|
||||||
steps: PlatformStepConfig[];
|
steps: PlatformStepConfig[];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user