ui+功能一样
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { handleBackgroundCommand, handleInstalled, handleStartup, handleWindowRemoved } from './service';
|
||||
import { broadcastCrawlStorageChange, handleExternalConnect, handleExternalMessage } from './service/externalBridge';
|
||||
import type { BackgroundCommand } from './types';
|
||||
import { cancelStaleCrawlWhenWindowMissing } from './service/crawlTask';
|
||||
import { getCrawlTaskState } from './service/taskState';
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
void handleInstalled();
|
||||
@@ -10,8 +12,22 @@ chrome.runtime.onStartup.addListener(() => {
|
||||
void handleStartup();
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((message: BackgroundCommand, _sender, sendResponse) => {
|
||||
void handleBackgroundMessage(message, sendResponse);
|
||||
chrome.runtime.onMessage.addListener((message: BackgroundCommand | { action?: string }, sender, sendResponse) => {
|
||||
if (message && typeof message === 'object' && message.action === 'GET_CRAWL_STATE_FOR_TAB') {
|
||||
void (async () => {
|
||||
await cancelStaleCrawlWhenWindowMissing();
|
||||
const state = await getCrawlTaskState();
|
||||
const tabId = sender.tab?.id;
|
||||
if (state && typeof tabId === 'number' && state.tabId === tabId) {
|
||||
sendResponse({ ok: true, data: state });
|
||||
return;
|
||||
}
|
||||
sendResponse({ ok: true, data: null });
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
|
||||
void handleBackgroundMessage(message as BackgroundCommand, sendResponse);
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ interface PageRunnerResponse {
|
||||
}
|
||||
|
||||
const activeCrawlControllers = new Map<string, AbortController>();
|
||||
const autoCloseTimers = new Map<string, number>();
|
||||
const DEFAULT_AUTOCLOSE_DELAY_MS = 10_000;
|
||||
|
||||
/**
|
||||
* 创建新的爬取任务,打开目标平台窗口,并把初始时间轴状态写入 storage。
|
||||
@@ -53,7 +55,16 @@ export async function startCrawl(platformId: string): Promise<CrawlStateResponse
|
||||
|
||||
try {
|
||||
const windowInfo = await createCrawlWindow(firstStep.url);
|
||||
const stateWithWindow = { ...nextState, windowId: windowInfo.id };
|
||||
let tabId: number | undefined;
|
||||
try {
|
||||
if (windowInfo.id) {
|
||||
tabId = await getWindowActiveTabId(windowInfo.id);
|
||||
}
|
||||
} catch {
|
||||
tabId = undefined;
|
||||
}
|
||||
|
||||
const stateWithWindow = { ...nextState, windowId: windowInfo.id, tabId };
|
||||
const controller = new AbortController();
|
||||
|
||||
await setCrawlTaskState(stateWithWindow);
|
||||
@@ -88,14 +99,26 @@ export async function cancelCrawl(): Promise<CrawlStateResponse> {
|
||||
}
|
||||
|
||||
abortActiveCrawl(state.id);
|
||||
clearAutoCloseTimer(state.id);
|
||||
|
||||
await clearCrawlTaskState();
|
||||
const canceledState: CrawlTaskState = {
|
||||
...state,
|
||||
status: 'canceled',
|
||||
autocloseAt: state.windowId ? Date.now() + DEFAULT_AUTOCLOSE_DELAY_MS : null,
|
||||
steps: state.steps.map((step, index) =>
|
||||
index === state.currentStepIndex && step.status === 'running'
|
||||
? { ...step, status: 'failed', message: '用户取消爬取任务' }
|
||||
: step,
|
||||
),
|
||||
};
|
||||
|
||||
if (state.windowId) {
|
||||
await chrome.windows.remove(state.windowId).catch(() => undefined);
|
||||
await setCrawlTaskState(canceledState);
|
||||
|
||||
if (canceledState.windowId) {
|
||||
scheduleAutoCloseWindow(canceledState.id, canceledState.windowId, canceledState.autocloseAt);
|
||||
}
|
||||
|
||||
return { ok: true, data: null };
|
||||
return { ok: true, data: canceledState };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,10 +155,12 @@ export async function cancelCrawlWhenWindowRemoved(windowId: number): Promise<vo
|
||||
}
|
||||
|
||||
abortActiveCrawl(state.id);
|
||||
clearAutoCloseTimer(state.id);
|
||||
|
||||
await setCrawlTaskState({
|
||||
...state,
|
||||
status: 'canceled',
|
||||
autocloseAt: null,
|
||||
steps: state.steps.map((step, index) =>
|
||||
index === state.currentStepIndex ? { ...step, status: 'failed', message: '爬取窗口已关闭' } : step,
|
||||
),
|
||||
@@ -156,10 +181,12 @@ export async function cancelStaleCrawlWhenWindowMissing(): Promise<void> {
|
||||
}
|
||||
|
||||
abortActiveCrawl(state.id);
|
||||
clearAutoCloseTimer(state.id);
|
||||
|
||||
await setCrawlTaskState({
|
||||
...state,
|
||||
status: 'canceled',
|
||||
autocloseAt: null,
|
||||
steps: state.steps.map((step, index) =>
|
||||
index === state.currentStepIndex ? { ...step, status: 'failed', message: '爬取窗口已关闭,任务已取消' } : step,
|
||||
),
|
||||
@@ -170,6 +197,68 @@ function abortActiveCrawl(taskId: string): void {
|
||||
activeCrawlControllers.get(taskId)?.abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消终态自动关窗(overlay“保持打开”)。
|
||||
*/
|
||||
export async function cancelAutoclose(): Promise<CrawlStateResponse> {
|
||||
const state = await getCrawlTaskState();
|
||||
|
||||
if (!state) {
|
||||
return { ok: true, data: null };
|
||||
}
|
||||
|
||||
clearAutoCloseTimer(state.id);
|
||||
|
||||
const nextState: CrawlTaskState = {
|
||||
...state,
|
||||
autocloseAt: null,
|
||||
};
|
||||
|
||||
await setCrawlTaskState(nextState);
|
||||
return { ok: true, data: nextState };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理当前任务快照(popup 的 Close/Dismiss)。不强制关窗,只影响 UI。
|
||||
*/
|
||||
export async function dismissCrawl(): Promise<CrawlStateResponse> {
|
||||
const state = await getCrawlTaskState();
|
||||
|
||||
if (!state) {
|
||||
return { ok: true, data: null };
|
||||
}
|
||||
|
||||
clearAutoCloseTimer(state.id);
|
||||
await clearCrawlTaskState();
|
||||
return { ok: true, data: null };
|
||||
}
|
||||
|
||||
function scheduleAutoCloseWindow(taskId: string, windowId: number, autocloseAt?: number | null): void {
|
||||
if (!autocloseAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearAutoCloseTimer(taskId);
|
||||
|
||||
const delayMs = Math.max(0, autocloseAt - Date.now());
|
||||
const timer = setTimeout(() => {
|
||||
autoCloseTimers.delete(taskId);
|
||||
chrome.windows.remove(windowId).catch(() => undefined);
|
||||
}, delayMs) as unknown as number;
|
||||
|
||||
autoCloseTimers.set(taskId, timer);
|
||||
}
|
||||
|
||||
function clearAutoCloseTimer(taskId: string): void {
|
||||
const timer = autoCloseTimers.get(taskId);
|
||||
if (timer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timer);
|
||||
autoCloseTimers.delete(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按平台 steps 顺序执行页面跳转、DOM 等待、字段抓取和进度更新。
|
||||
*/
|
||||
@@ -264,23 +353,37 @@ async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskSt
|
||||
}
|
||||
}
|
||||
|
||||
const autocloseAt = initialState.windowId ? Date.now() + DEFAULT_AUTOCLOSE_DELAY_MS : null;
|
||||
|
||||
await updateCrawlTaskState(initialState.id, (state) => ({
|
||||
...state,
|
||||
status: 'completed',
|
||||
autocloseAt,
|
||||
steps: state.steps.map((step) => (step.status === 'running' ? { ...step, status: 'success' } : step)),
|
||||
}));
|
||||
|
||||
if (initialState.windowId) {
|
||||
scheduleAutoCloseWindow(initialState.id, initialState.windowId, autocloseAt);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('[crawl] 执行失败', error);
|
||||
|
||||
const autocloseAt = initialState.windowId ? Date.now() + DEFAULT_AUTOCLOSE_DELAY_MS : null;
|
||||
|
||||
await updateCrawlTaskState(initialState.id, (state) => ({
|
||||
...state,
|
||||
status: 'failed',
|
||||
autocloseAt,
|
||||
steps: state.steps.map((step, index) =>
|
||||
index === state.currentStepIndex && step.status === 'running'
|
||||
? { ...step, status: 'failed', message: error instanceof Error ? error.message : '爬取执行失败' }
|
||||
: step,
|
||||
),
|
||||
}));
|
||||
|
||||
if (initialState.windowId) {
|
||||
scheduleAutoCloseWindow(initialState.id, initialState.windowId, autocloseAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,8 +520,9 @@ function createCrawlWindow(url: string): Promise<chrome.windows.Window> {
|
||||
chrome.windows.create(
|
||||
{
|
||||
url,
|
||||
type: 'normal',
|
||||
focused: true,
|
||||
type: 'popup',
|
||||
focused: false,
|
||||
state: 'normal',
|
||||
width: 1280,
|
||||
height: 900,
|
||||
},
|
||||
@@ -435,6 +539,7 @@ function createCrawlWindow(url: string): Promise<chrome.windows.Window> {
|
||||
return;
|
||||
}
|
||||
|
||||
void chrome.windows.update(windowInfo.id, { drawAttention: true }).catch(() => undefined);
|
||||
resolve(windowInfo);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import type { BackgroundCommand, BackgroundResponse, CrawlStateResponse } from '../types';
|
||||
import { cancelCrawl, cancelCrawlWhenWindowRemoved, cancelStaleCrawlWhenWindowMissing, resumeCrawl, startCrawl } from './crawlTask';
|
||||
import {
|
||||
cancelAutoclose,
|
||||
cancelCrawl,
|
||||
cancelCrawlWhenWindowRemoved,
|
||||
cancelStaleCrawlWhenWindowMissing,
|
||||
dismissCrawl,
|
||||
resumeCrawl,
|
||||
startCrawl,
|
||||
} from './crawlTask';
|
||||
import { getCrawlTaskState } from './taskState';
|
||||
|
||||
/**
|
||||
@@ -41,6 +49,10 @@ export async function handleBackgroundCommand(
|
||||
return cancelCrawl();
|
||||
case 'RESUME_CRAWL':
|
||||
return resumeCrawl();
|
||||
case 'CANCEL_AUTOCLOSE':
|
||||
return cancelAutoclose();
|
||||
case 'DISMISS_CRAWL':
|
||||
return dismissCrawl();
|
||||
default:
|
||||
return { ok: false, error: '未知的后台指令' };
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function getCrawlTaskState(): Promise<CrawlTaskState | null> {
|
||||
|
||||
export async function setCrawlTaskState(state: CrawlTaskState): Promise<void> {
|
||||
await chrome.storage.local.set({ [CRAWL_TASK_STORAGE_KEY]: state });
|
||||
broadcastToCrawlTab(state);
|
||||
}
|
||||
|
||||
export async function clearCrawlTaskState(): Promise<void> {
|
||||
@@ -29,6 +30,18 @@ export async function updateCrawlTaskState(
|
||||
await setCrawlTaskState(updater(state));
|
||||
}
|
||||
|
||||
function broadcastToCrawlTab(state: CrawlTaskState): void {
|
||||
if (!state.tabId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
void chrome.tabs.sendMessage(state.tabId, { type: 'crawl_state_update', state }).catch(() => undefined);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function isCrawlTaskState(value: unknown): value is CrawlTaskState {
|
||||
return typeof value === 'object' && value !== null && 'id' in value && 'steps' in value;
|
||||
}
|
||||
|
||||
@@ -29,8 +29,26 @@ export interface ResumeCrawlCommand {
|
||||
action: 'RESUME_CRAWL';
|
||||
}
|
||||
|
||||
// 取消终态自动关窗(保持窗口打开)的后台消息。
|
||||
export interface CancelAutocloseCommand {
|
||||
// 消息动作类型:用户在 overlay 中点“保持打开”,阻止 background 自动关闭爬取窗口。
|
||||
action: 'CANCEL_AUTOCLOSE';
|
||||
}
|
||||
|
||||
// 清理当前爬取任务快照(用于 popup 的 Dismiss/Close)。
|
||||
export interface DismissCrawlCommand {
|
||||
// 消息动作类型:清空 crawlTaskState,让 popup 回到 idle。
|
||||
action: 'DISMISS_CRAWL';
|
||||
}
|
||||
|
||||
// popup/content script 能发送给 background 的全部消息类型。
|
||||
export type BackgroundCommand = StartCrawlCommand | GetCrawlStateCommand | CancelCrawlCommand | ResumeCrawlCommand;
|
||||
export type BackgroundCommand =
|
||||
| StartCrawlCommand
|
||||
| GetCrawlStateCommand
|
||||
| CancelCrawlCommand
|
||||
| ResumeCrawlCommand
|
||||
| CancelAutocloseCommand
|
||||
| DismissCrawlCommand;
|
||||
|
||||
// background 统一响应结构。
|
||||
export interface BackgroundResponse<T = unknown> {
|
||||
|
||||
Reference in New Issue
Block a user