1
This commit is contained in:
@@ -29,7 +29,7 @@ export default defineManifest({
|
||||
},
|
||||
externally_connectable: {
|
||||
matches: [
|
||||
"http://localhost:3000/*",
|
||||
"http://localhost/*",
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vite-project",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
188
s.md
Normal file
188
s.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# 网站接入店闪扩展说明
|
||||
|
||||
这个扩展已经提供网站侧调用接口。网站点击“开始”时,可以让扩展执行和 popup 手动点击“立即爬取”一样的流程:打开新浏览器窗口、进入平台后台、抓取数据。抓取完成后,扩展会通过长连接把结果推回网站。
|
||||
|
||||
## 1. 先配置允许连接的网站域名
|
||||
|
||||
扩展的 `manifest.config.ts` 里有:
|
||||
|
||||
```ts
|
||||
externally_connectable: {
|
||||
matches: [
|
||||
"http://localhost:3000/*",
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
把你的网站域名加进去,例如:
|
||||
|
||||
```ts
|
||||
externally_connectable: {
|
||||
matches: [
|
||||
"http://localhost:3000/*",
|
||||
"https://your-site.com/*",
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
改完扩展后需要重新 `pnpm run build`,并在 Chrome 扩展管理页重新加载扩展。
|
||||
|
||||
## 2. 网站侧需要知道扩展 ID
|
||||
|
||||
Chrome 扩展管理页打开“开发者模式”,复制这个扩展的 ID:
|
||||
|
||||
```ts
|
||||
const EXTENSION_ID = "这里换成你的扩展ID";
|
||||
```
|
||||
|
||||
开发环境如果每次扩展 ID 变化,建议给扩展配置固定 key,或者每次复制新的 ID 到网站项目配置里。
|
||||
|
||||
## 3. 推荐的网站侧接入代码
|
||||
|
||||
网站页面加载后先建立长连接,用来接收扩展推送的进度和最终结果。
|
||||
|
||||
```ts
|
||||
const EXTENSION_ID = "这里换成你的扩展ID";
|
||||
|
||||
type DianshanMessage = {
|
||||
ok: boolean;
|
||||
type?: string;
|
||||
data?: {
|
||||
state: any | null;
|
||||
result: Record<string, unknown> | null;
|
||||
};
|
||||
error?: string;
|
||||
};
|
||||
|
||||
let port: chrome.runtime.Port | null = null;
|
||||
|
||||
export function connectDianshanExtension() {
|
||||
port = chrome.runtime.connect(EXTENSION_ID, { name: "DIANSHAN_CRAWL" });
|
||||
|
||||
port.onMessage.addListener((message: DianshanMessage) => {
|
||||
console.log("[dianshan]", message);
|
||||
|
||||
if (message.type === "DIANSHAN_CRAWL_STATE") {
|
||||
// 可选:更新网站上的进度 UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "DIANSHAN_CRAWL_DONE") {
|
||||
// 抓取完成,最终数据在 message.data.result
|
||||
console.log("抓取结果", message.data?.result);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "DIANSHAN_CRAWL_FAILED") {
|
||||
// 抓取失败,可展示 message.data.state.steps 里的失败原因
|
||||
console.error("抓取失败", message.data?.state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "DIANSHAN_CRAWL_CANCELED" || message.type === "DIANSHAN_CRAWL_CLEARED") {
|
||||
// 用户取消或任务被清空
|
||||
console.log("抓取已取消");
|
||||
}
|
||||
});
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
port = null;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 网站点击“开始抓取”
|
||||
|
||||
按钮点击时调用:
|
||||
|
||||
```ts
|
||||
export async function startDianshanCrawl(platformId = "Shopee") {
|
||||
const response = await chrome.runtime.sendMessage(EXTENSION_ID, {
|
||||
type: "DIANSHAN_START_CRAWL",
|
||||
payload: { platformId },
|
||||
});
|
||||
|
||||
if (!response?.ok) {
|
||||
throw new Error(response?.error ?? "启动抓取失败");
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
```
|
||||
|
||||
效果等同于用户打开 popup 后手动点击“立即爬取”。如果当前已经有 running/paused 的任务,扩展会直接返回当前任务,不会重复打开多个抓取窗口。
|
||||
|
||||
## 5. 查询当前状态
|
||||
|
||||
```ts
|
||||
export async function getDianshanCrawlState() {
|
||||
return chrome.runtime.sendMessage(EXTENSION_ID, {
|
||||
type: "DIANSHAN_GET_CRAWL_STATE",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 网站侧取消抓取
|
||||
|
||||
```ts
|
||||
export async function cancelDianshanCrawl() {
|
||||
return chrome.runtime.sendMessage(EXTENSION_ID, {
|
||||
type: "DIANSHAN_CANCEL_CRAWL",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
取消后扩展会清空 `crawlTaskState`,并关闭扩展自动打开的浏览器窗口。
|
||||
|
||||
## 7. 返回数据结构
|
||||
|
||||
长连接收到 `DIANSHAN_CRAWL_DONE` 时,数据大致是:
|
||||
|
||||
```ts
|
||||
{
|
||||
ok: true,
|
||||
type: "DIANSHAN_CRAWL_DONE",
|
||||
data: {
|
||||
state: {
|
||||
id: "Shopee-...",
|
||||
platformId: "Shopee",
|
||||
platformName: "Shopee 后台",
|
||||
status: "completed",
|
||||
steps: [
|
||||
{
|
||||
name: "数据看板",
|
||||
uniqueKey: "databoard",
|
||||
status: "success",
|
||||
result: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
result: {
|
||||
databoard: {
|
||||
name: "数据看板",
|
||||
status: "success",
|
||||
result: {}
|
||||
},
|
||||
adscenter: {
|
||||
name: "广告中心",
|
||||
status: "success",
|
||||
result: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
网站项目里一般用 `message.data.result` 入库或展示即可;如果要展示进度,用 `message.data.state.steps`。
|
||||
|
||||
## 8. 最小使用流程
|
||||
|
||||
```ts
|
||||
connectDianshanExtension();
|
||||
|
||||
document.querySelector("#start")?.addEventListener("click", async () => {
|
||||
await startDianshanCrawl("Shopee");
|
||||
});
|
||||
```
|
||||
|
||||
注意:网站必须运行在 `externally_connectable.matches` 配置过的域名下,否则 Chrome 会拒绝调用扩展。
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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: '平台配置不存在' };
|
||||
|
||||
207
src/background/service/externalBridge.ts
Normal file
207
src/background/service/externalBridge.ts
Normal 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;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"root":["./manifest.config.ts","./message.js","./vite.config.ts","./src/background/domscraper.ts","./src/background/index.ts","./src/background/service.ts","./src/background/types.ts","./src/background/service/crawltask.ts","./src/background/service/lifecycle.ts","./src/background/service/taskstate.ts","./src/config/platforms.ts","./src/content/app.vue","./src/content/main.ts","./src/content/pagerunner.ts","./src/options/app.vue","./src/options/main.ts","./src/popup/app.vue","./src/popup/main.ts","./src/popup/hook/use-login.ts","./src/popup/hook/use-scan.ts","./src/shared/auth.ts","./src/shared/message.ts","./src/shared/time_format.ts","./src/types/crawl.ts","./src/types/index.ts","./src/types/platform.ts"],"version":"5.9.3"}
|
||||
{"root":["./manifest.config.ts","./message.js","./vite.config.ts","./src/background/domscraper.ts","./src/background/index.ts","./src/background/service.ts","./src/background/types.ts","./src/background/service/crawltask.ts","./src/background/service/externalbridge.ts","./src/background/service/lifecycle.ts","./src/background/service/taskstate.ts","./src/config/platforms.ts","./src/content/app.vue","./src/content/main.ts","./src/content/pagerunner.ts","./src/options/app.vue","./src/options/main.ts","./src/popup/app.vue","./src/popup/main.ts","./src/popup/hook/use-login.ts","./src/popup/hook/use-scan.ts","./src/shared/auth.ts","./src/shared/message.ts","./src/shared/time_format.ts","./src/types/crawl.ts","./src/types/index.ts","./src/types/platform.ts"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user