From 08a6a69bd6818aa2267ca7f4e5a2c55c96b4aa9c Mon Sep 17 00:00:00 2001 From: zhu <1812073942@qq.com> Date: Thu, 30 Apr 2026 11:03:26 +0800 Subject: [PATCH] 1 --- src/background/service.ts | 151 +++++++++++++++++++++++++- src/background/types.ts | 28 ++++- src/content/App.vue | 220 +++++++++++++++++++++++++++++++++++++- src/content/main.ts | 9 +- src/popup/App.vue | 143 ++++++++++++++++++++----- src/types/crawl.ts | 45 ++++++++ src/types/index.ts | 7 ++ step.md | 3 +- tsconfig.tsbuildinfo | 2 +- 9 files changed, 566 insertions(+), 42 deletions(-) create mode 100644 src/types/crawl.ts diff --git a/src/background/service.ts b/src/background/service.ts index 4af9188..0ff8140 100644 --- a/src/background/service.ts +++ b/src/background/service.ts @@ -1,4 +1,8 @@ -import type { BackgroundCommand } from './types'; +import { getPlatformById } from '@/config/platforms'; +import type { CrawlProgressStep, CrawlTaskState } from '@/types'; +import type { BackgroundCommand, BackgroundResponse, CrawlStateResponse } from './types'; + +const CRAWL_TASK_STORAGE_KEY = 'crawlTaskState'; export async function handleInstalled(): Promise { console.log('[background] installed'); @@ -10,9 +14,148 @@ export async function handleStartup(): Promise { export async function handleWindowRemoved(windowId: number): Promise { console.log('[background] window removed', windowId); + + const state = await getCrawlTaskState(); + + if (state?.windowId === windowId && state.status === 'running') { + await setCrawlTaskState({ + ...state, + status: 'canceled', + steps: state.steps.map((step, index) => + index === state.currentStepIndex ? { ...step, status: 'failed', message: '爬取窗口已关闭' } : step, + ), + }); + } } -export async function handleBackgroundCommand(message: BackgroundCommand): Promise { - console.log('[background] message', message); - return { ok: true }; +export async function handleBackgroundCommand( + message: BackgroundCommand, +): Promise { + switch (message.action) { + case 'START_CRAWL': + return startCrawl(message.payload.platformId); + case 'GET_CRAWL_STATE': + return { ok: true, data: await getCrawlTaskState() }; + case 'CANCEL_CRAWL': + return cancelCrawl(); + default: + return { ok: false, error: '未知的后台指令' }; + } +} + +async function startCrawl(platformId: string): Promise { + const platform = getPlatformById(platformId); + + if (!platform) { + return { ok: false, error: '平台配置不存在' }; + } + + const startedAt = Date.now(); + const nextState: CrawlTaskState = { + id: `${platform.id}-${startedAt}`, + platformId: platform.id, + platformName: platform.name, + startedAt, + status: 'running', + currentStepIndex: 0, + steps: platform.steps.map((step, index) => ({ + name: step.name, + uniqueKey: step.uniqueKey, + status: index === 0 ? 'running' : 'pending', + })), + }; + + await setCrawlTaskState(nextState); + + try { + const windowInfo = await createCrawlWindow(platform.baseUrl); + const stateWithWindow = { ...nextState, windowId: windowInfo.id }; + await setCrawlTaskState(stateWithWindow); + return { ok: true, data: stateWithWindow }; + } catch (error: unknown) { + const failedState: CrawlTaskState = { + ...nextState, + status: 'failed', + steps: nextState.steps.map((step, index) => + index === 0 ? { ...step, status: 'failed', message: '打开平台窗口失败' } : step, + ), + }; + await setCrawlTaskState(failedState); + return { ok: false, data: failedState, error: error instanceof Error ? error.message : '打开平台窗口失败' }; + } +} + +async function cancelCrawl(): Promise { + const state = await getCrawlTaskState(); + + if (!state) { + return { ok: true, data: null }; + } + + const canceledState: CrawlTaskState = { + ...state, + status: 'canceled', + steps: state.steps.map((step, index) => + index === state.currentStepIndex ? { ...step, status: 'failed', message: '用户已取消' } : step, + ), + }; + + await setCrawlTaskState(canceledState); + + if (state.windowId) { + await removeWindow(state.windowId); + } + + return { ok: true, data: canceledState }; +} + +async function getCrawlTaskState(): Promise { + const result = await chrome.storage.local.get(CRAWL_TASK_STORAGE_KEY); + const state = result[CRAWL_TASK_STORAGE_KEY]; + return isCrawlTaskState(state) ? state : null; +} + +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( + { + url, + type: 'normal', + focused: true, + width: 1280, + height: 900, + }, + (windowInfo) => { + const runtimeError = chrome.runtime.lastError; + + if (runtimeError) { + reject(new Error(runtimeError.message)); + return; + } + + if (!windowInfo?.id) { + reject(new Error('窗口创建失败')); + return; + } + + resolve(windowInfo); + }, + ); + }); +} + +function removeWindow(windowId: number): Promise { + return new Promise((resolve) => { + chrome.windows.remove(windowId, () => { + resolve(); + }); + }); +} + +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 e37e380..a782421 100644 --- a/src/background/types.ts +++ b/src/background/types.ts @@ -1,4 +1,26 @@ -export interface BackgroundCommand { - action: string; - payload?: unknown; +import type { CrawlTaskState } from '@/types'; + +export interface StartCrawlCommand { + action: 'START_CRAWL'; + payload: { + platformId: string; + }; } + +export interface GetCrawlStateCommand { + action: 'GET_CRAWL_STATE'; +} + +export interface CancelCrawlCommand { + action: 'CANCEL_CRAWL'; +} + +export type BackgroundCommand = StartCrawlCommand | GetCrawlStateCommand | CancelCrawlCommand; + +export interface BackgroundResponse { + ok: boolean; + data?: T; + error?: string; +} + +export type CrawlStateResponse = BackgroundResponse; diff --git a/src/content/App.vue b/src/content/App.vue index 23da7b4..428dbb7 100644 --- a/src/content/App.vue +++ b/src/content/App.vue @@ -1,5 +1,219 @@ - + + + + + diff --git a/src/content/main.ts b/src/content/main.ts index 18e5949..f63fc1e 100644 --- a/src/content/main.ts +++ b/src/content/main.ts @@ -1,7 +1,7 @@ import { createApp } from 'vue'; import App from './App.vue'; -/** 将内容脚本应用挂载到页面的 Shadow DOM 中。 */ +/** 将内容脚本应用挂载到页面中。 */ function mountApp() { if (document.getElementById('dianshan-crx-root')) { return; @@ -9,11 +9,10 @@ function mountApp() { const container = document.createElement('div'); container.id = 'dianshan-crx-root'; - - const shadowRoot = container.attachShadow({ mode: 'open' }); const appRoot = document.createElement('div'); - shadowRoot.appendChild(appRoot); - document.documentElement.appendChild(container); + + container.appendChild(appRoot); + document.body.appendChild(container); createApp(App).mount(appRoot); } diff --git a/src/popup/App.vue b/src/popup/App.vue index 2f7ffa9..aacf2af 100644 --- a/src/popup/App.vue +++ b/src/popup/App.vue @@ -1,13 +1,17 @@