Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c4fbf6310 | |||
| ef620109cf |
@@ -3,7 +3,9 @@
|
||||
AiToEarn 自维护的 Chrome/Edge 扩展,也是 Gitea Release 唯一允许打包发布的扩展源码,用于:
|
||||
|
||||
- 从当前网页采集标题、正文和图片。
|
||||
- 在普通 HTTP 采集不足时复用当前 Chrome 标签页,等待动态页面渲染后回传正文、HTML、图片和链接。
|
||||
- 在 AiToEarn 页面提供 `window.AIToEarnDistribution` 分发桥接。
|
||||
- 在 AiToEarn 页面提供独立的 `window.AIToEarnInteraction` 互动读取桥接,不冒充旧官方扩展的完整能力。
|
||||
- 复用浏览器现有登录状态检测内容平台账号。
|
||||
- 将同一份 Markdown、HTML 或纯文本内容填入平台编辑器。
|
||||
- 扩展弹窗从 COSE 适配器动态读取平台列表,并可将当前网页内容打开、填充到所选平台编辑器。
|
||||
@@ -12,6 +14,10 @@ AiToEarn 自维护的 Chrome/Edge 扩展,也是 Gitea Release 唯一允许打
|
||||
|
||||
扩展弹窗和后台通知中的“成功”只表示平台编辑器已打开并完成内容填充,不表示内容已经公开发布。右键菜单“提取此页面到 AiToEarn”会把最近一次提取结果暂存到浏览器本地,随后打开扩展即可继续选择平台。
|
||||
|
||||
AiToEarn 后台触发浏览器采集时会优先复用同地址标签页;没有现成标签页时才在当前 Chrome 内创建后台标签页。普通采集完成后临时标签页会关闭,遇到登录、验证码或地区访问限制时则保留并激活页面,等待用户处理。
|
||||
|
||||
当前互动桥接只开放受控白名单:能力查询、抖音评论列表和抖音回复列表。作品 ID、评论 ID、游标和数量均由扩展校验,网页不能向扩展传入任意请求地址。扩展优先复用当前 Chrome 中已有的抖音标签页,没有可复用标签页时才创建非激活标签页,并在抖音页面上下文内发起带现有 Cookie 的请求。
|
||||
|
||||
## 上游来源
|
||||
|
||||
多平台检测与编辑器填充逻辑基于 `doocs/cose`,固定版本和许可证见:
|
||||
|
||||
+123
@@ -1,6 +1,113 @@
|
||||
import './distribution/cose/background.js';
|
||||
import './interaction/background.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' });
|
||||
@@ -86,6 +193,22 @@ chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||
});
|
||||
|
||||
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 platformIds = Array.isArray(request.platformIds)
|
||||
|
||||
+112
-41
@@ -3,17 +3,47 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
if (globalThis.__AI_TO_EARN_CONTENT_SCRIPT__) return;
|
||||
globalThis.__AI_TO_EARN_CONTENT_SCRIPT__ = true;
|
||||
|
||||
const contentSelectors = [
|
||||
'article',
|
||||
'[role="main"]',
|
||||
'main',
|
||||
'.amos-land-page',
|
||||
'.post-content',
|
||||
'.article-content',
|
||||
'.entry-content',
|
||||
'.content-body',
|
||||
'.markdown-body',
|
||||
'.rich_media_content',
|
||||
'#article-content',
|
||||
'#content',
|
||||
'.content',
|
||||
'.post',
|
||||
'.article',
|
||||
'.story-body',
|
||||
'#cnblogs_post_body',
|
||||
'.htmledit_views',
|
||||
'#article_content',
|
||||
'.blog-content-box'
|
||||
];
|
||||
|
||||
// ===== Content Extraction =====
|
||||
function extractPageContent() {
|
||||
const data = {
|
||||
title: '',
|
||||
description: '',
|
||||
body: '',
|
||||
html: '',
|
||||
images: [],
|
||||
links: [],
|
||||
url: window.location.href,
|
||||
siteName: '',
|
||||
author: '',
|
||||
publishDate: ''
|
||||
publishDate: '',
|
||||
pageType: 'article',
|
||||
accessState: { status: 'ok', message: '', requiresUserAction: false }
|
||||
};
|
||||
|
||||
// --- Title ---
|
||||
@@ -43,10 +73,15 @@
|
||||
|| '';
|
||||
|
||||
// --- Body Content ---
|
||||
data.body = extractMainContent();
|
||||
const mainContent = extractMainContent();
|
||||
data.body = mainContent.text.slice(0, 100000);
|
||||
data.html = mainContent.html.slice(0, 900000);
|
||||
|
||||
// --- Images ---
|
||||
data.images = extractImages();
|
||||
data.images = extractImages(mainContent.element);
|
||||
data.links = extractLinks(mainContent.element);
|
||||
data.pageType = detectPageType();
|
||||
data.accessState = detectAccessState(data.body);
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -58,34 +93,12 @@
|
||||
|
||||
// ===== Main Content Extraction =====
|
||||
function extractMainContent() {
|
||||
// Priority selectors for main content areas
|
||||
const contentSelectors = [
|
||||
'article',
|
||||
'[role="main"]',
|
||||
'main',
|
||||
'.post-content',
|
||||
'.article-content',
|
||||
'.entry-content',
|
||||
'.content-body',
|
||||
'.markdown-body',
|
||||
'#article-content',
|
||||
'#content',
|
||||
'.content',
|
||||
'.post',
|
||||
'.article',
|
||||
'.story-body',
|
||||
'#cnblogs_post_body',
|
||||
'.htmledit_views',
|
||||
'#article_content',
|
||||
'.blog-content-box'
|
||||
];
|
||||
|
||||
for (const selector of contentSelectors) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
const text = cleanText(el.innerText);
|
||||
if (text.length > 50) {
|
||||
return text;
|
||||
return { text, html: el.outerHTML, element: el };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,16 +110,66 @@
|
||||
.map(p => p.textContent.trim())
|
||||
.filter(t => t.length > 20);
|
||||
if (texts.length > 0) {
|
||||
return cleanText(texts.join('\n\n'));
|
||||
return {
|
||||
text: cleanText(texts.join('\n\n')),
|
||||
html: `<main>${Array.from(paragraphs).map(p => p.outerHTML).join('')}</main>`,
|
||||
element: document.body
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback
|
||||
return cleanText(document.body.innerText);
|
||||
return {
|
||||
text: cleanText(document.body?.innerText || ''),
|
||||
html: document.body?.innerHTML || '',
|
||||
element: document.body
|
||||
};
|
||||
}
|
||||
|
||||
function detectPageType() {
|
||||
const route = `${window.location.pathname}${window.location.search}`.toLowerCase();
|
||||
if (/\/trending\/|\/topic(?:\/|$)|topic_innerflow/.test(route)
|
||||
|| document.querySelector('.amos-land-page, [data-page-type="topic"]')) {
|
||||
return 'topic';
|
||||
}
|
||||
if (/\/(?:search|tag|category|hot|feed)(?:\/|$)/.test(window.location.pathname.toLowerCase())
|
||||
|| document.querySelector('[data-page-type="listing"]')) {
|
||||
return 'listing';
|
||||
}
|
||||
return 'article';
|
||||
}
|
||||
|
||||
function detectAccessState(extractedBody) {
|
||||
const visible = element => Boolean(element && (element.offsetWidth || element.offsetHeight || element.getClientRects().length));
|
||||
const pageText = cleanText(document.body?.innerText || '').slice(0, 12000);
|
||||
const lowerText = pageText.toLowerCase();
|
||||
const captchaElement = Array.from(document.querySelectorAll(
|
||||
'[class*="captcha"], [id*="captcha"], [class*="verify"], [id*="verify"], iframe[src*="captcha"]'
|
||||
)).find(visible);
|
||||
if (captchaElement || /(验证码|安全验证|人机验证|滑块验证|verify you are human|complete the security check|captcha)/i.test(pageText)) {
|
||||
return { status: 'captcha_required', message: '页面需要完成验证码或安全验证', requiresUserAction: true };
|
||||
}
|
||||
|
||||
const passwordInput = Array.from(document.querySelectorAll('input[type="password"]')).find(visible);
|
||||
const loginRoute = /\/(?:login|signin|passport)(?:\/|$)/i.test(window.location.pathname);
|
||||
if (passwordInput || (loginRoute && /(登录|登陆|sign in|log in)/i.test(pageText))) {
|
||||
return { status: 'login_required', message: '页面需要登录后才能采集', requiresUserAction: true };
|
||||
}
|
||||
|
||||
if (extractedBody.length < 120 && (
|
||||
lowerText.includes('access denied')
|
||||
|| lowerText.includes('forbidden')
|
||||
|| lowerText.includes('not available in your region')
|
||||
|| /(所在地区|当前地区|地区限制|需要.*vpn|网络环境受限)/i.test(pageText)
|
||||
)) {
|
||||
return { status: 'access_restricted', message: '页面存在地区或网络访问限制', requiresUserAction: true };
|
||||
}
|
||||
|
||||
return { status: 'ok', message: '', requiresUserAction: false };
|
||||
}
|
||||
|
||||
// ===== Image Extraction =====
|
||||
function extractImages() {
|
||||
function extractImages(mainElement) {
|
||||
const images = new Set();
|
||||
|
||||
// 1. Open Graph image
|
||||
@@ -121,18 +184,17 @@
|
||||
// Skip favicons, they're not useful as content images
|
||||
|
||||
// 4. Content area images (from article/main/content areas)
|
||||
const contentSelectors = ['article', 'main', '.content', '.post-content', '#content'];
|
||||
for (const selector of contentSelectors) {
|
||||
const container = document.querySelector(selector);
|
||||
if (container) {
|
||||
container.querySelectorAll('img').forEach(img => {
|
||||
const src = img.src || img.dataset.src || img.dataset.original;
|
||||
if (src && !isSmallImage(img) && !isIconOrAvatar(src)) {
|
||||
images.add(resolveUrl(src));
|
||||
}
|
||||
});
|
||||
if (images.size > 0) break;
|
||||
}
|
||||
if (mainElement) {
|
||||
mainElement.querySelectorAll('img').forEach(img => {
|
||||
const src = img.currentSrc
|
||||
|| img.src
|
||||
|| img.dataset.src
|
||||
|| img.dataset.original
|
||||
|| img.dataset.lazySrc;
|
||||
if (src && !isSmallImage(img) && !isIconOrAvatar(src)) {
|
||||
images.add(resolveUrl(src));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 5. If no content images found, scan all images
|
||||
@@ -148,6 +210,15 @@
|
||||
return Array.from(images).slice(0, 10);
|
||||
}
|
||||
|
||||
function extractLinks(mainElement) {
|
||||
const links = new Set();
|
||||
(mainElement || document.body)?.querySelectorAll('a[href]').forEach(anchor => {
|
||||
const href = resolveUrl(anchor.getAttribute('href'));
|
||||
if (/^https?:\/\//i.test(href)) links.add(href);
|
||||
});
|
||||
return Array.from(links).slice(0, 100);
|
||||
}
|
||||
|
||||
function isSmallImage(img) {
|
||||
return (img.naturalWidth > 0 && img.naturalWidth < 100) ||
|
||||
(img.naturalHeight > 0 && img.naturalHeight < 100);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
'getVersion',
|
||||
'listPlatforms',
|
||||
'checkPlatforms',
|
||||
'captureUrl',
|
||||
'startBatch',
|
||||
'syncToPlatform',
|
||||
]);
|
||||
@@ -33,6 +34,11 @@
|
||||
const response = await sendRuntimeMessage({ type: 'CHECK_PLATFORM_STATUS', platforms: selected });
|
||||
return response?.status || {};
|
||||
}
|
||||
case 'captureUrl': {
|
||||
if (typeof payload?.url !== 'string' || !payload.url.trim())
|
||||
throw new Error('缺少要采集的页面地址');
|
||||
return chrome.runtime.sendMessage({ type: 'CAPTURE_URL', url: payload.url });
|
||||
}
|
||||
case 'startBatch':
|
||||
return sendRuntimeMessage({ type: 'START_SYNC_BATCH' });
|
||||
case 'syncToPlatform': {
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
getVersion: () => request('getVersion'),
|
||||
listPlatforms: () => request('listPlatforms'),
|
||||
checkPlatforms: platformIds => request('checkPlatforms', { platformIds }),
|
||||
captureUrl: url => request('captureUrl', { url }),
|
||||
startBatch: () => request('startBatch'),
|
||||
syncToPlatform: (platformId, content) => request('syncToPlatform', { platformId, content }),
|
||||
},
|
||||
|
||||
@@ -868,7 +868,7 @@ async function syncToPlatform(platformId, content) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.plainText || content.markdown || content.body || ''
|
||||
console.log('[COSE] 小红书 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 填充标题和内容
|
||||
@@ -1583,7 +1583,7 @@ async function syncToPlatform(platformId, content) {
|
||||
// 微信公众号:直接注入 HTML 到编辑器
|
||||
if (platformId === 'wechat') {
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.wechatHtml || content.html || content.body || ''
|
||||
console.log('[COSE] 微信 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 等待额外时间确保编辑器完全加载
|
||||
@@ -1753,7 +1753,7 @@ async function syncToPlatform(platformId, content) {
|
||||
// 抖音:使用剪贴板 HTML 粘贴到编辑器(类似微信公众号)
|
||||
if (platformId === 'douyin') {
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.plainText || content.markdown || content.body || ''
|
||||
console.log('[COSE] 抖音 HTML 内容长度:', htmlContent?.length || 0)
|
||||
console.log('[COSE] 开始注入抖音内容...')
|
||||
|
||||
@@ -1883,7 +1883,7 @@ async function syncToPlatform(platformId, content) {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.html || content.body || ''
|
||||
console.log('[COSE] 搜狐号 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 填充标题和内容
|
||||
@@ -1942,7 +1942,7 @@ async function syncToPlatform(platformId, content) {
|
||||
// B站专栏:使用 UEditor execCommand 插入 HTML
|
||||
if (platformId === 'bilibili') {
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.html || content.body || ''
|
||||
console.log('[COSE] B站专栏 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 等待 UEditor 就绪
|
||||
@@ -2052,7 +2052,7 @@ async function syncToPlatform(platformId, content) {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.plainText || content.markdown || content.body || ''
|
||||
console.log('[COSE] 微博头条 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 填充标题和内容
|
||||
@@ -2717,7 +2717,7 @@ async function syncToPlatform(platformId, content) {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.html || content.body || ''
|
||||
console.log('[COSE] 百家号 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 填充标题和内容
|
||||
@@ -2826,7 +2826,7 @@ async function syncToPlatform(platformId, content) {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.html || content.body || ''
|
||||
console.log('[COSE] 少数派 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 填充标题和内容
|
||||
@@ -2896,7 +2896,7 @@ async function syncToPlatform(platformId, content) {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.html || content.body || ''
|
||||
console.log('[COSE] 支付宝开放平台 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 填充标题和内容
|
||||
@@ -3196,7 +3196,7 @@ async function syncToPlatform(platformId, content) {
|
||||
// 豆瓣:向首页分享框注入内容
|
||||
if (platformId === 'douban') {
|
||||
// 使用纯文本内容(豆瓣分享框不支持富文本)
|
||||
const textContent = content.markdown || content.body || ''
|
||||
const textContent = content.plainText || content.markdown || content.body || ''
|
||||
console.log('[COSE] 豆瓣文本内容长度:', textContent?.length || 0)
|
||||
|
||||
// 等待页面加载
|
||||
@@ -3389,7 +3389,7 @@ async function syncToPlatform(platformId, content) {
|
||||
|
||||
// 在目标页面执行的填充函数
|
||||
function fillContentOnPage(content, platformId) {
|
||||
const { title, body, markdown, wechatHtml } = content
|
||||
const { title, body, html, markdown, plainText } = content
|
||||
|
||||
// 等待元素出现的工具函数
|
||||
function waitFor(selector, timeout = 10000) {
|
||||
@@ -3422,7 +3422,7 @@ function fillContentOnPage(content, platformId) {
|
||||
// 根据平台填充内容
|
||||
async function fill() {
|
||||
const host = window.location.hostname
|
||||
const contentToFill = markdown || body || ''
|
||||
const contentToFill = markdown || plainText || body || ''
|
||||
|
||||
// 知乎专栏 - 由 syncToPlatform 单独处理(使用导入文档功能)
|
||||
if (host.includes('zhihu.com')) {
|
||||
@@ -3839,7 +3839,7 @@ function fillContentOnPage(content, platformId) {
|
||||
* Medium 使用 contenteditable 编辑器,通过 paste 事件注入 HTML 内容
|
||||
* 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
*/
|
||||
const htmlContent = wechatHtml || body || ''
|
||||
const htmlContent = html || body || ''
|
||||
const contentEl = document.querySelector('p.graf--p')
|
||||
|
||||
if (contentEl && htmlContent) {
|
||||
|
||||
@@ -19,8 +19,8 @@ const MediumPlatform = {
|
||||
* 3. 通过 paste 事件填充 HTML 内容到编辑器
|
||||
*/
|
||||
async function fillMediumContent(content, waitFor, setInputValue) {
|
||||
const { title, body, wechatHtml } = content
|
||||
const htmlContent = wechatHtml || body || ''
|
||||
const { title, body, html } = content
|
||||
const htmlContent = html || body || ''
|
||||
|
||||
console.log('[COSE] Medium 开始同步...')
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ async function syncWangyihaoContent(tab, content, helpers) {
|
||||
await injectUtils(chrome, tab.id)
|
||||
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.html || content.body || ''
|
||||
console.log('[COSE] 网易号 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 在页面中执行:填充标题和粘贴 HTML 内容
|
||||
|
||||
@@ -331,7 +331,7 @@ async function syncWechatContent(tab, content, helpers) {
|
||||
await waitForTab(tab.id)
|
||||
|
||||
// 使用剪贴板 HTML(带完整样式)或降级到 body
|
||||
const htmlContent = content.wechatHtml || content.body
|
||||
const htmlContent = content.wechatHtml || content.html || content.body || ''
|
||||
console.log('[COSE] 微信 HTML 内容长度:', htmlContent?.length || 0)
|
||||
|
||||
// 步骤4:使用 MutationObserver 监听编辑器出现
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
(function bridgeAiToEarnInteraction() {
|
||||
const requestSource = 'aitoearn-interaction-web';
|
||||
const responseSource = 'aitoearn-interaction-extension';
|
||||
const allowedMethods = new Set([
|
||||
'getCapabilities',
|
||||
'listDouyinComments',
|
||||
'listDouyinReplies',
|
||||
]);
|
||||
|
||||
async function handle(method, payload) {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'AITO_EARN_INTERACTION',
|
||||
method,
|
||||
payload,
|
||||
});
|
||||
|
||||
if (!response?.success) {
|
||||
const error = new Error(response?.error?.message || 'AiToEarn 互动扩展请求失败');
|
||||
error.code = response?.error?.code || 'INTERACTION_REQUEST_FAILED';
|
||||
throw error;
|
||||
}
|
||||
return response.result;
|
||||
}
|
||||
|
||||
window.addEventListener('message', async (event) => {
|
||||
if (
|
||||
event.source !== window
|
||||
|| event.origin !== window.location.origin
|
||||
|| event.data?.source !== requestSource
|
||||
) return;
|
||||
|
||||
const { requestId, method, payload } = event.data;
|
||||
if (!requestId || !allowedMethods.has(method)) return;
|
||||
|
||||
try {
|
||||
const result = await handle(method, payload);
|
||||
window.postMessage({ source: responseSource, requestId, result }, window.location.origin);
|
||||
} catch (error) {
|
||||
window.postMessage({
|
||||
source: responseSource,
|
||||
requestId,
|
||||
error: {
|
||||
code: error?.code || 'INTERACTION_REQUEST_FAILED',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
}, window.location.origin);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,65 @@
|
||||
(function installAiToEarnInteractionBridge() {
|
||||
if (window.AIToEarnInteraction) return;
|
||||
|
||||
const requestSource = 'aitoearn-interaction-web';
|
||||
const responseSource = 'aitoearn-interaction-extension';
|
||||
const pending = new Map();
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (
|
||||
event.source !== window
|
||||
|| event.origin !== window.location.origin
|
||||
|| event.data?.source !== responseSource
|
||||
) return;
|
||||
|
||||
const entry = pending.get(event.data.requestId);
|
||||
if (!entry) return;
|
||||
pending.delete(event.data.requestId);
|
||||
|
||||
if (event.data.error) {
|
||||
const error = new Error(event.data.error.message || 'AiToEarn 互动扩展请求失败');
|
||||
error.code = event.data.error.code || 'INTERACTION_REQUEST_FAILED';
|
||||
entry.reject(error);
|
||||
return;
|
||||
}
|
||||
entry.resolve(event.data.result);
|
||||
});
|
||||
|
||||
function request(method, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = crypto.randomUUID();
|
||||
const timeout = window.setTimeout(() => {
|
||||
pending.delete(requestId);
|
||||
const error = new Error('AiToEarn 互动扩展响应超时');
|
||||
error.code = 'INTERACTION_RESPONSE_TIMEOUT';
|
||||
reject(error);
|
||||
}, 60000);
|
||||
|
||||
pending.set(requestId, {
|
||||
resolve(value) {
|
||||
window.clearTimeout(timeout);
|
||||
resolve(value);
|
||||
},
|
||||
reject(error) {
|
||||
window.clearTimeout(timeout);
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
|
||||
window.postMessage({ source: requestSource, requestId, method, payload }, window.location.origin);
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'AIToEarnInteraction', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: {
|
||||
getCapabilities: () => request('getCapabilities'),
|
||||
listDouyinComments: payload => request('listDouyinComments', payload),
|
||||
listDouyinReplies: payload => request('listDouyinReplies', payload),
|
||||
},
|
||||
});
|
||||
|
||||
window.dispatchEvent(new CustomEvent('aitoearn:interaction-ready'));
|
||||
})();
|
||||
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
executeDouyinCommentRequest,
|
||||
InteractionValidationError,
|
||||
normalizeDouyinRequest,
|
||||
} from './douyin.js';
|
||||
|
||||
const MESSAGE_TYPE = 'AITO_EARN_INTERACTION';
|
||||
const ALLOWED_APP_ORIGINS = new Set([
|
||||
'http://localhost:6061',
|
||||
'http://127.0.0.1:6061',
|
||||
'https://wx.frp.it1024.cc',
|
||||
]);
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
function createError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
function isAllowedSender(sender) {
|
||||
const senderUrl = sender?.url || sender?.tab?.url;
|
||||
if (!senderUrl) return false;
|
||||
try {
|
||||
return ALLOWED_APP_ORIGINS.has(new URL(senderUrl).origin);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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 tab;
|
||||
await sleep(250);
|
||||
}
|
||||
throw createError('DOUYIN_PAGE_TIMEOUT', '抖音页面加载超时');
|
||||
}
|
||||
|
||||
async function acquireDouyinTab(workId) {
|
||||
const workUrl = `https://www.douyin.com/video/${workId}`;
|
||||
const tabs = await chrome.tabs.query({ url: ['https://www.douyin.com/*'] });
|
||||
let tab = tabs.find(candidate => candidate.id && candidate.url?.startsWith(workUrl));
|
||||
if (!tab) tab = tabs.find(candidate => candidate.id && !candidate.discarded);
|
||||
|
||||
const created = !tab;
|
||||
if (!tab) tab = await chrome.tabs.create({ url: workUrl, active: false });
|
||||
if (!tab.id) throw createError('DOUYIN_TAB_UNAVAILABLE', '无法创建或复用抖音标签页');
|
||||
|
||||
await waitForTabReady(tab.id);
|
||||
return { tabId: tab.id, created };
|
||||
}
|
||||
|
||||
async function listDouyinComments(method, payload) {
|
||||
const request = normalizeDouyinRequest(method, payload);
|
||||
const tab = await acquireDouyinTab(request.workId);
|
||||
let keepTabOpen = false;
|
||||
|
||||
try {
|
||||
const execution = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.tabId },
|
||||
world: 'MAIN',
|
||||
func: executeDouyinCommentRequest,
|
||||
args: [{
|
||||
...request,
|
||||
kind: method === 'listDouyinReplies' ? 'replies' : 'comments',
|
||||
}],
|
||||
});
|
||||
const response = execution?.[0]?.result;
|
||||
if (!response?.ok) {
|
||||
const code = response?.error?.code || 'DOUYIN_REQUEST_FAILED';
|
||||
const message = response?.error?.message || '抖音评论读取失败';
|
||||
if (code === 'DOUYIN_LOGIN_REQUIRED') {
|
||||
keepTabOpen = true;
|
||||
await chrome.tabs.update(tab.tabId, { active: true });
|
||||
}
|
||||
throw createError(code, message);
|
||||
}
|
||||
|
||||
return {
|
||||
...response.result,
|
||||
reusedTab: !tab.created,
|
||||
};
|
||||
} finally {
|
||||
if (tab.created && !keepTabOpen) {
|
||||
await chrome.tabs.remove(tab.tabId).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCapabilities() {
|
||||
return {
|
||||
version: chrome.runtime.getManifest().version,
|
||||
source: 'aitoearn-interaction',
|
||||
platforms: {
|
||||
douyin: {
|
||||
comments: {
|
||||
list: true,
|
||||
replies: true,
|
||||
create: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request?.type !== MESSAGE_TYPE) return false;
|
||||
|
||||
if (!isAllowedSender(sender)) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: { code: 'INTERACTION_ORIGIN_DENIED', message: '当前页面无权调用互动扩展' },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const method = request.method;
|
||||
if (!['getCapabilities', 'listDouyinComments', 'listDouyinReplies'].includes(method)) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: { code: 'INTERACTION_METHOD_DENIED', message: '不支持的互动扩展方法' },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
Promise.resolve(method === 'getCapabilities'
|
||||
? getCapabilities()
|
||||
: listDouyinComments(method, request.payload))
|
||||
.then(result => sendResponse({ success: true, result }))
|
||||
.catch((error) => {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: {
|
||||
code: error?.code || (error instanceof InteractionValidationError
|
||||
? 'INVALID_INTERACTION_PAYLOAD'
|
||||
: 'INTERACTION_REQUEST_FAILED'),
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
});
|
||||
});
|
||||
return true;
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
const ID_PATTERN = /^\d{1,32}$/;
|
||||
const CURSOR_PATTERN = /^\d{1,16}$/;
|
||||
const DEFAULT_COUNT = 20;
|
||||
const MAX_COUNT = 20;
|
||||
|
||||
export class InteractionValidationError extends Error {
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
this.name = 'InteractionValidationError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeId(value, fieldName) {
|
||||
const id = typeof value === 'number' ? String(value) : String(value || '').trim();
|
||||
if (!ID_PATTERN.test(id)) {
|
||||
throw new InteractionValidationError('INVALID_DOUYIN_ID', `${fieldName} 必须是纯数字 ID`);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function normalizeCursor(value) {
|
||||
if (value === undefined || value === null || value === '') return '0';
|
||||
const cursor = typeof value === 'number' ? String(value) : String(value).trim();
|
||||
if (!CURSOR_PATTERN.test(cursor)) {
|
||||
throw new InteractionValidationError('INVALID_DOUYIN_CURSOR', '分页游标必须是非负整数');
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
function normalizeCount(value) {
|
||||
if (value === undefined || value === null || value === '') return DEFAULT_COUNT;
|
||||
const count = Number(value);
|
||||
if (!Number.isInteger(count) || count < 1) {
|
||||
throw new InteractionValidationError('INVALID_DOUYIN_COUNT', '每页数量必须是正整数');
|
||||
}
|
||||
return Math.min(count, MAX_COUNT);
|
||||
}
|
||||
|
||||
export function normalizeDouyinRequest(method, payload) {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
throw new InteractionValidationError('INVALID_INTERACTION_PAYLOAD', '互动请求参数格式不正确');
|
||||
}
|
||||
|
||||
const normalized = {
|
||||
workId: normalizeId(payload.workId, '作品 ID'),
|
||||
cursor: normalizeCursor(payload.cursor),
|
||||
count: normalizeCount(payload.count),
|
||||
};
|
||||
|
||||
if (method === 'listDouyinReplies') {
|
||||
normalized.commentId = normalizeId(payload.commentId, '评论 ID');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export async function executeDouyinCommentRequest(request) {
|
||||
function getAvatarUrl(user) {
|
||||
const candidates = [
|
||||
...(user?.avatar_thumb?.url_list || []),
|
||||
...(user?.avatar_medium?.url_list || []),
|
||||
];
|
||||
return candidates.find(url => typeof url === 'string' && url.startsWith('https://')) || '';
|
||||
}
|
||||
|
||||
function toFiniteNumber(value, fallback = 0) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : fallback;
|
||||
}
|
||||
|
||||
function normalizeComment(comment, parentCommentId) {
|
||||
const id = String(comment?.cid || '');
|
||||
const nestedReplies = Array.isArray(comment?.reply_comment)
|
||||
? comment.reply_comment.map(reply => normalizeComment(reply, id)).filter(reply => reply.id)
|
||||
: [];
|
||||
const createdAtSeconds = toFiniteNumber(comment?.create_time);
|
||||
|
||||
return {
|
||||
id,
|
||||
workId: String(comment?.aweme_id || request.workId),
|
||||
parentCommentId: parentCommentId || undefined,
|
||||
rootCommentId: String(parentCommentId || comment?.root_comment_id || id || ''),
|
||||
text: typeof comment?.text === 'string' ? comment.text : '',
|
||||
createdAt: createdAtSeconds > 0 ? new Date(createdAtSeconds * 1000).toISOString() : undefined,
|
||||
likeCount: toFiniteNumber(comment?.digg_count),
|
||||
replyCount: toFiniteNumber(
|
||||
parentCommentId
|
||||
? comment?.reply_comment_total
|
||||
: comment?.reply_comment_total ?? comment?.comment_reply_total,
|
||||
nestedReplies.length,
|
||||
),
|
||||
ipLabel: typeof comment?.ip_label === 'string' ? comment.ip_label : '',
|
||||
isAuthor: comment?.label_text === '作者' || Number(comment?.label_type) === 1,
|
||||
author: {
|
||||
name: typeof comment?.user?.nickname === 'string' ? comment.user.nickname : '',
|
||||
uid: String(comment?.user?.uid || ''),
|
||||
secUid: String(comment?.user?.sec_uid || ''),
|
||||
avatarUrl: getAvatarUrl(comment?.user),
|
||||
},
|
||||
replies: nestedReplies,
|
||||
};
|
||||
}
|
||||
|
||||
function getBrowserVersion() {
|
||||
const match = navigator.userAgent.match(/(?:Chrome|Edg)\/(\d+(?:\.\d+){0,3})/);
|
||||
return match?.[1] || '';
|
||||
}
|
||||
|
||||
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||
const commonParams = {
|
||||
device_platform: 'webapp',
|
||||
aid: '6383',
|
||||
channel: 'channel_pc_web',
|
||||
item_type: '0',
|
||||
cut_version: '1',
|
||||
update_version_code: '170400',
|
||||
pc_client_type: '1',
|
||||
pc_libra_divert: 'Windows',
|
||||
support_h265: '1',
|
||||
support_dash: '1',
|
||||
cpu_core_num: String(navigator.hardwareConcurrency || 8),
|
||||
version_code: '170400',
|
||||
version_name: '17.4.0',
|
||||
cookie_enabled: String(navigator.cookieEnabled),
|
||||
screen_width: String(screen.width || 1920),
|
||||
screen_height: String(screen.height || 1080),
|
||||
browser_language: navigator.language || 'zh-CN',
|
||||
browser_platform: navigator.platform || 'Win32',
|
||||
browser_name: navigator.userAgent.includes('Edg/') ? 'Edge' : 'Chrome',
|
||||
browser_version: getBrowserVersion(),
|
||||
browser_online: String(navigator.onLine),
|
||||
engine_name: 'Blink',
|
||||
os_name: 'Windows',
|
||||
device_memory: String(navigator.deviceMemory || 8),
|
||||
platform: 'PC',
|
||||
downlink: String(connection?.downlink || 10),
|
||||
effective_type: connection?.effectiveType || '4g',
|
||||
round_trip_time: String(connection?.rtt || 50),
|
||||
};
|
||||
|
||||
const isReplies = request.kind === 'replies';
|
||||
const params = new URLSearchParams({
|
||||
...commonParams,
|
||||
cursor: request.cursor,
|
||||
count: String(request.count),
|
||||
...(isReplies
|
||||
? { item_id: request.workId, comment_id: request.commentId }
|
||||
: { aweme_id: request.workId }),
|
||||
});
|
||||
const endpoint = isReplies
|
||||
? '/aweme/v1/web/comment/list/reply/'
|
||||
: '/aweme/v1/web/comment/list/';
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 25000);
|
||||
try {
|
||||
const response = await fetch(`${endpoint}?${params}`, {
|
||||
credentials: 'include',
|
||||
headers: { accept: 'application/json, text/plain, */*' },
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: response.status === 401 || response.status === 403
|
||||
? 'DOUYIN_LOGIN_REQUIRED'
|
||||
: 'DOUYIN_HTTP_ERROR',
|
||||
message: `抖音评论接口返回 HTTP ${response.status}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (Number(data?.status_code) !== 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'DOUYIN_API_ERROR',
|
||||
message: data?.status_msg || `抖音评论接口返回状态 ${data?.status_code}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
result: {
|
||||
platform: 'douyin',
|
||||
workId: request.workId,
|
||||
parentCommentId: isReplies ? request.commentId : undefined,
|
||||
items: (Array.isArray(data.comments) ? data.comments : [])
|
||||
.map(comment => normalizeComment(comment, isReplies ? request.commentId : undefined))
|
||||
.filter(comment => comment.id),
|
||||
cursor: String(data.cursor ?? ''),
|
||||
hasMore: Boolean(data.has_more),
|
||||
total: Number.isFinite(Number(data.total)) ? Number(data.total) : undefined,
|
||||
source: 'douyin-web',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: error?.name === 'AbortError' ? 'DOUYIN_REQUEST_TIMEOUT' : 'DOUYIN_REQUEST_FAILED',
|
||||
message: error?.name === 'AbortError'
|
||||
? '抖音评论请求超时,请确认页面可以正常访问后重试'
|
||||
: (error instanceof Error ? error.message : String(error)),
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "AiToEarn - 内容营销助手",
|
||||
"version": "1.1.0",
|
||||
"description": "AiToEarn 自维护内容扩展:网页采集、账号检测与多平台文章分发",
|
||||
"version": "1.3.0",
|
||||
"description": "AiToEarn 自维护内容扩展:网页采集、账号检测、多平台文章分发与受控互动读取",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"clipboardRead",
|
||||
@@ -83,7 +83,7 @@
|
||||
"http://127.0.0.1:6061/*",
|
||||
"https://wx.frp.it1024.cc/*"
|
||||
],
|
||||
"js": ["distribution-page-bridge.js"],
|
||||
"js": ["distribution-page-bridge.js", "interaction-page-bridge.js"],
|
||||
"run_at": "document_start",
|
||||
"world": "MAIN"
|
||||
},
|
||||
@@ -93,7 +93,7 @@
|
||||
"http://127.0.0.1:6061/*",
|
||||
"https://wx.frp.it1024.cc/*"
|
||||
],
|
||||
"js": ["distribution-bridge.js"],
|
||||
"js": ["distribution-bridge.js", "interaction-bridge.js"],
|
||||
"run_at": "document_start"
|
||||
},
|
||||
{
|
||||
|
||||
+9
-1
@@ -17,6 +17,8 @@ $requiredPaths = @(
|
||||
'content.js',
|
||||
'distribution-bridge.js',
|
||||
'distribution-page-bridge.js',
|
||||
'interaction-bridge.js',
|
||||
'interaction-page-bridge.js',
|
||||
'popup.html',
|
||||
'popup.css',
|
||||
'popup.js',
|
||||
@@ -24,7 +26,8 @@ $requiredPaths = @(
|
||||
'options.css',
|
||||
'options.js',
|
||||
'icons',
|
||||
'distribution'
|
||||
'distribution',
|
||||
'interaction'
|
||||
)
|
||||
|
||||
foreach ($relativePath in $requiredPaths) {
|
||||
@@ -47,6 +50,11 @@ foreach ($javascriptFile in $javascriptFiles) {
|
||||
}
|
||||
}
|
||||
|
||||
& node (Join-Path $PSScriptRoot 'test-interaction.mjs')
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Interaction bridge validation failed'
|
||||
}
|
||||
|
||||
$distRoot = Join-Path $repoRoot 'dist'
|
||||
$stageRoot = Join-Path $distRoot "aitoearn-extension-v$version"
|
||||
$archivePath = Join-Path $distRoot "aitoearn-extension-v$version.zip"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { InteractionValidationError, normalizeDouyinRequest } from '../interaction/douyin.js';
|
||||
|
||||
assert.deepEqual(
|
||||
normalizeDouyinRequest('listDouyinComments', { workId: '7570305000069549352' }),
|
||||
{ workId: '7570305000069549352', cursor: '0', count: 20 },
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
normalizeDouyinRequest('listDouyinReplies', {
|
||||
workId: '7570305000069549352',
|
||||
commentId: '7570539465648243462',
|
||||
cursor: 20,
|
||||
count: 999,
|
||||
}),
|
||||
{
|
||||
workId: '7570305000069549352',
|
||||
commentId: '7570539465648243462',
|
||||
cursor: '20',
|
||||
count: 20,
|
||||
},
|
||||
);
|
||||
|
||||
for (const payload of [
|
||||
{ workId: 'https://example.com/' },
|
||||
{ workId: '7570305000069549352', cursor: '-1' },
|
||||
{ workId: '7570305000069549352', count: 0 },
|
||||
]) {
|
||||
assert.throws(
|
||||
() => normalizeDouyinRequest('listDouyinComments', payload),
|
||||
error => error instanceof InteractionValidationError,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Interaction bridge validation passed.');
|
||||
Reference in New Issue
Block a user