242 lines
7.4 KiB
JavaScript
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;
|
|
});
|