ui+功能一样

This commit is contained in:
zhu
2026-05-11 17:24:53 +08:00
parent 6677ec5eec
commit f8972f72ba
15 changed files with 1453 additions and 168 deletions

View File

@@ -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;
});

View File

@@ -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);
},
);

View File

@@ -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: '未知的后台指令' };
}

View File

@@ -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;
}

View File

@@ -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> {