Files
chrome-extension/background.js
T

242 lines
7.4 KiB
JavaScript

import './distribution/cose/background.js';
import './interaction/background.js';
import './distribution/browser-agent/index.js';
import { AUTO_FILL_PLATFORM_IDS } from './distribution/cose/core/platforms/index.js';
const PREPARE_BATCH_MESSAGE = 'PREPARE_PLATFORM_BATCH';
const CAPTURE_URL_MESSAGE = 'CAPTURE_URL';
const CAPTURE_ATTEMPTS = 8;
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
function normalizeCaptureUrl(value) {
const url = new URL(value);
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('只支持采集 HTTP 或 HTTPS 页面');
if (url.username || url.password) throw new Error('采集地址不能包含账号或密码');
url.hash = '';
return url;
}
function comparableUrl(value) {
try {
return normalizeCaptureUrl(value).href.replace(/\/$/, '');
} catch {
return '';
}
}
async function waitForTabReady(tabId) {
const deadline = Date.now() + 30000;
while (Date.now() < deadline) {
const tab = await chrome.tabs.get(tabId);
if (tab.status === 'complete') return;
await sleep(300);
}
throw new Error('页面加载超时');
}
async function requestPageExtraction(tabId) {
try {
return await chrome.tabs.sendMessage(tabId, { action: 'extractContent' });
} catch {
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] });
return chrome.tabs.sendMessage(tabId, { action: 'extractContent' });
}
}
async function captureUrl(rawUrl) {
const requestedUrl = normalizeCaptureUrl(rawUrl);
const requestedComparableUrl = comparableUrl(requestedUrl.href);
const tabs = await chrome.tabs.query({});
let tab = tabs.find(candidate => candidate.id && comparableUrl(candidate.url) === requestedComparableUrl);
const createdTab = !tab;
let keepTabOpen = false;
if (!tab) {
tab = await chrome.tabs.create({ url: requestedUrl.href, active: false });
}
if (!tab.id) throw new Error('无法创建或复用采集标签页');
try {
await waitForTabReady(tab.id);
let lastBodyLength = 0;
let lastTitle = '';
for (let attempt = 1; attempt <= CAPTURE_ATTEMPTS; attempt += 1) {
const response = await requestPageExtraction(tab.id);
if (!response?.success) throw new Error(response?.error || '扩展无法读取页面内容');
const data = response.data || {};
const accessState = data.accessState || { status: 'ok', message: '', requiresUserAction: false };
if (accessState.requiresUserAction) {
keepTabOpen = true;
await chrome.tabs.update(tab.id, { active: true });
return {
success: false,
error: accessState.message || '页面需要人工操作后才能继续采集',
requiresUserAction: true,
accessState,
tabId: tab.id,
reusedTab: !createdTab,
};
}
const bodyLength = typeof data.body === 'string' ? data.body.trim().length : 0;
const minimumLength = data.pageType === 'article' ? 120 : 60;
lastBodyLength = bodyLength;
lastTitle = data.title || '';
if (bodyLength >= minimumLength) {
return {
success: true,
data: {
...data,
url: requestedUrl.href,
finalUrl: data.url || requestedUrl.href,
},
tabId: tab.id,
reusedTab: !createdTab,
};
}
await sleep(Math.min(2500, 600 + attempt * 250));
}
throw new Error(`页面已打开,但动态正文仍未渲染完成(标题:${lastTitle || '未知'},正文 ${lastBodyLength} 字)`);
} finally {
if (createdTab && !keepTabOpen) {
await chrome.tabs.remove(tab.id).catch(() => undefined);
}
}
}
async function preparePlatformBatch(platformIds, content) {
await chrome.runtime.sendMessage({ type: 'START_SYNC_BATCH' });
const results = [];
for (const platformId of platformIds) {
try {
const result = await chrome.runtime.sendMessage({
type: 'SYNC_TO_PLATFORM',
platformId,
content,
});
const success = Boolean(result) && result.success !== false && !result.error;
results.push({
platformId,
success,
message: result?.message || '',
error: result?.error || '',
tabId: result?.tabId,
});
} catch (error) {
results.push({
platformId,
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
const successCount = results.filter(result => result.success).length;
const total = platformIds.length;
const message = successCount > 0
? `已打开并填充 ${successCount}/${total} 个平台,请逐个平台检查后手动确认发布。`
: `未能填充所选的 ${total} 个平台,请检查登录状态后重试。`;
await chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'AiToEarn 内容分发',
message,
});
return {
success: successCount > 0,
successCount,
total,
message,
results,
};
}
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.removeAll(() => {
chrome.contextMenus.create({
id: 'aitoearn-extract',
title: '提取此页面到 AiToEarn',
contexts: ['page', 'link'],
});
});
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId !== 'aitoearn-extract' || !tab?.id) return;
chrome.tabs.sendMessage(tab.id, { action: 'extractContent' }, (response) => {
if (!response?.success) return;
const data = response.data;
chrome.storage.local.set({
pendingExtract: {
...data,
extractedAt: Date.now(),
},
}).then(() => {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'AiToEarn 已提取页面',
message: `已提取“${data.title.substring(0, 50)}”,打开扩展即可选择平台。`,
});
});
});
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === CAPTURE_URL_MESSAGE) {
if (typeof request.url !== 'string' || !request.url.trim()) {
sendResponse({ success: false, error: '缺少要采集的页面地址' });
return false;
}
captureUrl(request.url)
.then(sendResponse)
.catch((error) => {
sendResponse({
success: false,
error: error instanceof Error ? error.message : String(error),
});
});
return true;
}
if (request.type !== PREPARE_BATCH_MESSAGE) return false;
const requestedPlatformIds = Array.isArray(request.platformIds)
? request.platformIds.filter(platformId => typeof platformId === 'string' && platformId.trim())
: [];
const platformIds = [...new Set(requestedPlatformIds)]
.filter(platformId => AUTO_FILL_PLATFORM_IDS.has(platformId));
if (platformIds.length === 0 || !request.content || typeof request.content !== 'object') {
sendResponse({
success: false,
error: requestedPlatformIds.length > 0
? '所选平台暂未开放受控自动填充,请使用对应平台的复制发布稿。'
: '缺少目标平台或分发内容',
});
return false;
}
preparePlatformBatch(platformIds, request.content)
.then(sendResponse)
.catch((error) => {
sendResponse({
success: false,
error: error instanceof Error ? error.message : String(error),
});
});
return true;
});