This commit is contained in:
zhu
2026-05-09 18:51:44 +08:00
parent 186840ba23
commit 6677ec5eec
7 changed files with 414 additions and 7 deletions

View File

@@ -1,4 +1,5 @@
import { handleBackgroundCommand, handleInstalled, handleStartup, handleWindowRemoved } from './service';
import { broadcastCrawlStorageChange, handleExternalConnect, handleExternalMessage } from './service/externalBridge';
import type { BackgroundCommand } from './types';
chrome.runtime.onInstalled.addListener(() => {
@@ -19,16 +20,22 @@ chrome.windows.onRemoved.addListener((windowId) => {
});
chrome.runtime.onMessageExternal.addListener((message, _sender, sendResponse) => {
if (message.type === 'STORE_AI_PING') {
void handleExternalMessage(message).then(sendResponse).catch((error: unknown) => {
sendResponse({
success: true,
version: chrome.runtime.getManifest().version,
ok: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
return true;
});
chrome.runtime.onConnectExternal.addListener(handleExternalConnect);
chrome.storage.onChanged.addListener((changes, areaName) => {
broadcastCrawlStorageChange(changes, areaName);
});
/**
* Wrap background command handling so async errors can still be returned to callers.
*/

View File

@@ -18,6 +18,11 @@ const activeCrawlControllers = new Map<string, AbortController>();
*/
export async function startCrawl(platformId: string): Promise<CrawlStateResponse> {
const platform = getPlatformById(platformId);
const currentState = await getCrawlTaskState();
if (currentState && ['running', 'paused'].includes(currentState.status)) {
return { ok: true, data: currentState };
}
if (!platform) {
return { ok: false, error: '平台配置不存在' };

View File

@@ -0,0 +1,207 @@
import { platformConfigs } from '@/config/platforms';
import type { CrawlTaskState } from '@/types';
import { cancelCrawl, startCrawl } from './crawlTask';
import { getCrawlTaskState } from './taskState';
const CRAWL_TASK_STORAGE_KEY = 'crawlTaskState';
const EXTERNAL_PORT_NAME = 'DIANSHAN_CRAWL';
type ExternalAction =
| 'DIANSHAN_PING'
| 'DIANSHAN_START_CRAWL'
| 'DIANSHAN_GET_CRAWL_STATE'
| 'DIANSHAN_CANCEL_CRAWL'
| 'STORE_AI_PING';
interface ExternalMessage {
type?: ExternalAction;
action?: ExternalAction;
payload?: {
platformId?: string;
};
}
interface ExternalResponse<T = unknown> {
ok: boolean;
success?: boolean;
type?: string;
data?: T;
error?: string;
}
interface CrawlWebPayload {
state: CrawlTaskState | null;
result: Record<string, unknown> | null;
}
const externalPorts = new Set<chrome.runtime.Port>();
export async function handleExternalMessage(message: ExternalMessage): Promise<ExternalResponse> {
const action = message.type ?? message.action;
switch (action) {
case 'STORE_AI_PING':
case 'DIANSHAN_PING':
return {
ok: true,
success: true,
data: {
version: chrome.runtime.getManifest().version,
platforms: platformConfigs.map((platform) => ({
id: platform.id,
name: platform.name,
})),
},
};
case 'DIANSHAN_START_CRAWL':
return startCrawlForWebsite(message.payload?.platformId);
case 'DIANSHAN_GET_CRAWL_STATE':
return {
ok: true,
data: buildCrawlWebPayload(await getCrawlTaskState()),
};
case 'DIANSHAN_CANCEL_CRAWL':
await cancelCrawl();
return {
ok: true,
data: buildCrawlWebPayload(null),
};
default:
return { ok: false, error: 'unknown_external_action' };
}
}
export function handleExternalConnect(port: chrome.runtime.Port): void {
if (port.name !== EXTERNAL_PORT_NAME) {
port.disconnect();
return;
}
externalPorts.add(port);
getCrawlTaskState()
.then((state) => {
postToExternalPort(port, {
ok: true,
type: 'DIANSHAN_CRAWL_STATE',
data: buildCrawlWebPayload(state),
});
})
.catch((error: unknown) => {
postToExternalPort(port, {
ok: false,
type: 'DIANSHAN_CRAWL_ERROR',
error: error instanceof Error ? error.message : String(error),
});
});
port.onMessage.addListener((message: ExternalMessage) => {
void handleExternalMessage(message)
.then((response) => {
postToExternalPort(port, response);
})
.catch((error: unknown) => {
postToExternalPort(port, {
ok: false,
type: 'DIANSHAN_CRAWL_ERROR',
error: error instanceof Error ? error.message : String(error),
});
});
});
port.onDisconnect.addListener(() => {
externalPorts.delete(port);
});
}
export function broadcastCrawlStorageChange(changes: Record<string, chrome.storage.StorageChange>, areaName: string): void {
if (areaName !== 'local') {
return;
}
const change = changes[CRAWL_TASK_STORAGE_KEY];
if (!change) {
return;
}
const nextState = isCrawlTaskState(change.newValue) ? change.newValue : null;
const oldState = isCrawlTaskState(change.oldValue) ? change.oldValue : null;
const type = getBroadcastType(nextState, oldState);
broadcastToExternalPorts({
ok: true,
type,
data: buildCrawlWebPayload(nextState),
});
}
async function startCrawlForWebsite(platformId?: string): Promise<ExternalResponse<CrawlWebPayload>> {
const response = await startCrawl(platformId ?? platformConfigs[0]?.id ?? '');
return {
ok: response.ok,
type: 'DIANSHAN_CRAWL_STARTED',
data: buildCrawlWebPayload(response.data ?? null),
error: response.error,
};
}
function buildCrawlWebPayload(state: CrawlTaskState | null): CrawlWebPayload {
return {
state,
result: state?.status === 'completed' ? collectStepResults(state) : null,
};
}
function collectStepResults(state: CrawlTaskState): Record<string, unknown> {
return Object.fromEntries(
state.steps.map((step) => [
step.uniqueKey,
{
name: step.name,
status: step.status,
result: step.result ?? null,
message: step.message ?? null,
},
]),
);
}
function getBroadcastType(nextState: CrawlTaskState | null, oldState: CrawlTaskState | null): string {
if (!nextState) {
return oldState ? 'DIANSHAN_CRAWL_CLEARED' : 'DIANSHAN_CRAWL_STATE';
}
if (nextState.status === 'completed') {
return 'DIANSHAN_CRAWL_DONE';
}
if (nextState.status === 'failed') {
return 'DIANSHAN_CRAWL_FAILED';
}
if (nextState.status === 'canceled') {
return 'DIANSHAN_CRAWL_CANCELED';
}
return 'DIANSHAN_CRAWL_STATE';
}
function broadcastToExternalPorts(message: ExternalResponse<CrawlWebPayload>): void {
for (const port of externalPorts) {
postToExternalPort(port, message);
}
}
function postToExternalPort(port: chrome.runtime.Port, message: ExternalResponse): void {
try {
port.postMessage(message);
} catch {
externalPorts.delete(port);
}
}
function isCrawlTaskState(value: unknown): value is CrawlTaskState {
return typeof value === 'object' && value !== null && 'id' in value && 'steps' in value;
}