mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-02-02 21:23:38 +08:00
feat: enhance waitForPageLoaded
This commit is contained in:
parent
6f15a8bcb5
commit
cb249dba87
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "azon-seeker",
|
||||
"displayName": "Azon Seeker v0.7.1.2-beta",
|
||||
"displayName": "Azon Seeker v0.7.1.4-beta",
|
||||
"version": "0.7.2",
|
||||
"private": true,
|
||||
"description": "Starter modify by honestfox101 and PetrichorFun",
|
||||
|
||||
@ -10,6 +10,8 @@ export function useLongTask() {
|
||||
try {
|
||||
result = await task();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
console.error('Task failed:', error);
|
||||
}
|
||||
isRunning.value = false;
|
||||
|
||||
@ -15,6 +15,7 @@ export function isForbiddenUrl(url: string): boolean {
|
||||
export const isFirefox = navigator.userAgent.includes('Firefox');
|
||||
|
||||
// export const remoteHost = __DEV__ ? '127.0.0.1:8000' : '47.251.4.191:8000';
|
||||
export const remoteHost = __DEV__ ? '127.0.0.1:18000' : 'vm8nc3zr-18000.usw2.devtunnels.ms';
|
||||
// export const remoteHost = __DEV__ ? '127.0.0.1:18000' : 'vm8nc3zr-18000.usw2.devtunnels.ms';
|
||||
export const remoteHost = __DEV__ ? '127.0.0.1:18000' : '47.251.4.191:8000';
|
||||
|
||||
// export const remoteHost = '47.251.4.191:8000';
|
||||
|
||||
@ -48,6 +48,11 @@ export async function exec<T, P extends Record<string, unknown>>(
|
||||
): Promise<T> {
|
||||
const { timeout = 30000 } = options;
|
||||
return new Promise<T>(async (resolve, reject) => {
|
||||
// 基本刷新
|
||||
// await browser.tabs.reload(tabId);
|
||||
// console.log('exec', browser.tabs.get(tabId), tabId);
|
||||
|
||||
// console.log('exec', func, (await browser.tabs.get(tabId)).url);
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await new Promise<void>((r) => setTimeout(r, 200));
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
@ -55,7 +60,24 @@ export async function exec<T, P extends Record<string, unknown>>(
|
||||
break;
|
||||
}
|
||||
}
|
||||
setTimeout(() => reject('脚本运行超时'), timeout);
|
||||
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
if (tab.status !== 'complete') {
|
||||
console.log('waitForPageLoaded');
|
||||
|
||||
await browser.tabs.reload(tabId);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // 等待刷新开始
|
||||
}
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await new Promise<void>((r) => setTimeout(r, 200));
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
if (tab.status === 'complete') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => reject(`脚本运行超时, foucs on ${func}`), timeout);
|
||||
try {
|
||||
const injectResults = await browser.scripting.executeScript({
|
||||
target: { tabId },
|
||||
|
||||
@ -6,24 +6,177 @@ export class AmazonSearchPageInjector extends BaseInjector {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||
while (true) {
|
||||
const targetNode = document.querySelector('.s-pagination-next');
|
||||
window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 50) + 500));
|
||||
|
||||
const h = Math.max(
|
||||
document.documentElement.scrollHeight,
|
||||
document.body.scrollHeight,
|
||||
document.documentElement.offsetHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.clientHeight,
|
||||
);
|
||||
|
||||
await modernScrollTo({ top: h * (0.4 + Math.random() * 0.2), behavior: 'smooth' });
|
||||
|
||||
if (targetNode || document.readyState === 'complete') {
|
||||
targetNode?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 50) + 250));
|
||||
|
||||
// 第一段:滚到中下部
|
||||
// window.scrollTo({
|
||||
// top: h * (1 - ~~(Math.random() * 50)),
|
||||
// behavior: 'smooth'
|
||||
// });
|
||||
await modernScrollTo({ top: h * (0.9 + Math.random() * 0.08), behavior: 'smooth' });
|
||||
await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 50) + 250));
|
||||
|
||||
// targetNode?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
await scrollIntoViewAndWait(targetNode as HTMLElement, {
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 50) + 250));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||
const spins = Array.from(document.querySelectorAll<HTMLElement>('.a-spinner')).filter(
|
||||
(e) => e.getClientRects().length > 0,
|
||||
);
|
||||
if (spins.length === 0) {
|
||||
// const spins = Array.from(document.querySelectorAll<HTMLElement>('.a-spinner')).filter(
|
||||
// (e) => e.getClientRects().length > 0,
|
||||
// );
|
||||
// if (spins.length === 0) {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// const spins = Array.from(document.querySelectorAll('.a-carousel-card-empty'))
|
||||
// if (spins.length === 0) {
|
||||
// break;
|
||||
// }
|
||||
|
||||
const pagination = document.querySelectorAll('.rhf-sign-in-button');
|
||||
if (pagination.length > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑滚动并等待滚动结束
|
||||
* @param options 原生 ScrollToOptions 接口 (包含 top, left, behavior)
|
||||
*/
|
||||
async function modernScrollTo(options: ScrollToOptions): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
// 1. 针对已经处于目标位置的情况做兜底处理
|
||||
// 如果目标位置与当前位置一致,部分浏览器可能不会触发 scrollend
|
||||
const targetTop = options.top ?? window.scrollY;
|
||||
const targetLeft = options.left ?? window.scrollX;
|
||||
|
||||
if (targetTop === window.scrollY && targetLeft === window.scrollX) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 监听原生滚动结束事件
|
||||
// 注意:scrollend 是 2023-2024 年起普及的标准,2026 年已是主流
|
||||
document.addEventListener(
|
||||
'scrollend',
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
// 3. 执行滚动
|
||||
window.scrollTo(options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将元素滚动到视野中心并等待动画结束
|
||||
* @param element 目标 DOM 元素
|
||||
* @param options 滚动配置
|
||||
*/
|
||||
async function scrollIntoViewAndWait(
|
||||
element: HTMLElement,
|
||||
options: ScrollIntoViewOptions = { behavior: 'smooth', block: 'center' },
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
// 监听滚动结束事件
|
||||
// scrollend 会在滚动圆满完成或被中断时触发
|
||||
document.addEventListener(
|
||||
'scrollend',
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
// 触发滚动
|
||||
element.scrollIntoView(options);
|
||||
|
||||
// 兼容性兜底:如果元素已经在视野内,浏览器可能不触发滚动
|
||||
// 可以在此处检查位置,若无位移则直接 resolve
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// public waitForPageLoaded() {
|
||||
// return this.run(async () => {
|
||||
// await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||
|
||||
// // 第一个 while 循环:添加 12 秒超时刷新机制
|
||||
// const timeoutMs = 12000; // 12 秒
|
||||
// let startTime = Date.now();
|
||||
// let refreshCount = 0;
|
||||
// const maxRefresh = 1; // 最多刷新 1 次
|
||||
// console.log('waitForPageLoaded');
|
||||
|
||||
// while (true) {
|
||||
// const targetNode = document.querySelector('.s-pagination-next');
|
||||
// window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
||||
// await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 50) + 500));
|
||||
|
||||
// if (targetNode || document.readyState === 'complete') {
|
||||
// targetNode?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// break;
|
||||
// }
|
||||
|
||||
// // 检查是否超时
|
||||
// if (Date.now() - startTime > timeoutMs) {
|
||||
// if (refreshCount < maxRefresh) {
|
||||
// // 刷新页面
|
||||
// console.log('检查是否超时,刷新页面');
|
||||
|
||||
// browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {
|
||||
// browser.tabs.reload(tabs[0].id);
|
||||
// });
|
||||
|
||||
// await new Promise((resolve) => setTimeout(resolve, 2000)); // 等待刷新开始
|
||||
|
||||
// // 重置计时器和刷新计数
|
||||
// refreshCount++;
|
||||
// startTime = Date.now();
|
||||
// continue; // 重新从 while 循环开始
|
||||
// } else {
|
||||
// // 已刷新过一次,仍然超时,抛出错误
|
||||
// throw new Error('等待页面加载超时(已尝试刷新)');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 第二个 while 循环:等待 spinner 消失
|
||||
// while (true) {
|
||||
// await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||
// const spins = Array.from(document.querySelectorAll('.a-spinner')).filter(
|
||||
// (e) => e.getClientRects().length > 0
|
||||
// );
|
||||
// if (spins.length === 0) {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
public async getPagePattern() {
|
||||
return this.run(async () => {
|
||||
@ -118,32 +271,32 @@ export class AmazonSearchPageInjector extends BaseInjector {
|
||||
});
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 检测当前亚马逊搜索页面是否有下一页,并自动点击翻页按钮。
|
||||
// *
|
||||
// * 该方法在页面上下文中执行,查找亚马逊标准分页按钮('.s-pagination-next'),
|
||||
// * 检查按钮是否未被禁用('s-pagination-disabled' 类),然后模拟用户点击。
|
||||
// * 点击前会随机等待 500-1000 毫秒以避免被识别为机器人。
|
||||
// *
|
||||
// * @returns `true` 表示有下一页且已点击翻页按钮,页面正在加载下一页内容;
|
||||
// * `false` 表示没有下一页(按钮不存在或被禁用)。
|
||||
// */
|
||||
// public async determineHasNextPage() {
|
||||
// return this.run(async () => {
|
||||
// const nextButton = document.querySelector<HTMLLinkElement>('.s-pagination-next');
|
||||
// if (nextButton) {
|
||||
// if (!nextButton.classList.contains('s-pagination-disabled')) {
|
||||
// await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||
// nextButton.click();
|
||||
// return true;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
/**
|
||||
* 检测当前亚马逊搜索页面是否有下一页,并自动点击翻页按钮。
|
||||
*
|
||||
* 该方法在页面上下文中执行,查找亚马逊标准分页按钮('.s-pagination-next'),
|
||||
* 检查按钮是否未被禁用('s-pagination-disabled' 类),然后模拟用户点击。
|
||||
* 点击前会随机等待 500-1000 毫秒以避免被识别为机器人。
|
||||
*
|
||||
* @returns `true` 表示有下一页且已点击翻页按钮,页面正在加载下一页内容;
|
||||
* `false` 表示没有下一页(按钮不存在或被禁用)。
|
||||
*/
|
||||
public async determineHasNextPage() {
|
||||
return this.run(async () => {
|
||||
const nextButton = document.querySelector<HTMLLinkElement>('.s-pagination-next');
|
||||
if (nextButton) {
|
||||
if (!nextButton.classList.contains('s-pagination-disabled')) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||
nextButton.click();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// /**
|
||||
// * 检测并执行亚马逊搜索页面翻页,等待页面刷新完成。
|
||||
// * @returns 能否翻页(true=翻页成功,false=没有下一页或翻页失败)
|
||||
@ -178,51 +331,51 @@ export class AmazonSearchPageInjector extends BaseInjector {
|
||||
* 检测并执行亚马逊搜索页面翻页,通过 URL 变化确认刷新完成。
|
||||
* @returns 能否翻页(true=已翻页,false=已是最后一页)
|
||||
*/
|
||||
public async determineHasNextPage(): Promise<boolean> {
|
||||
return this.run(async () => {
|
||||
const nextButton = document.querySelector<HTMLLinkElement>('.s-pagination-next');
|
||||
// public async determineHasNextPage(): Promise<boolean> {
|
||||
// return this.run(async () => {
|
||||
// const nextButton = document.querySelector<HTMLLinkElement>('.s-pagination-next');
|
||||
|
||||
if (nextButton && !nextButton.classList.contains('s-pagination-disabled')) {
|
||||
// 1. 记录当前 URL
|
||||
const initialUrl = window.location.href;
|
||||
// if (nextButton && !nextButton.classList.contains('s-pagination-disabled')) {
|
||||
// // 1. 记录当前 URL
|
||||
// const initialUrl = window.location.href;
|
||||
|
||||
// 2. 随机等待后点击
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||
nextButton.click();
|
||||
// // 2. 随机等待后点击
|
||||
// await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||
// nextButton.click();
|
||||
|
||||
// 3. 等待 URL 变化(表示页面已开始导航)
|
||||
await new Promise<void>((resolve) => {
|
||||
const checkUrl = () => {
|
||||
if (window.location.href !== initialUrl) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkUrl, 100);
|
||||
}
|
||||
};
|
||||
checkUrl();
|
||||
});
|
||||
// // 3. 等待 URL 变化(表示页面已开始导航)
|
||||
// await new Promise<void>((resolve) => {
|
||||
// const checkUrl = () => {
|
||||
// if (window.location.href !== initialUrl) {
|
||||
// resolve();
|
||||
// } else {
|
||||
// setTimeout(checkUrl, 100);
|
||||
// }
|
||||
// };
|
||||
// checkUrl();
|
||||
// });
|
||||
|
||||
// 4. 等待页面稳定(document.readyState === 'complete')
|
||||
await new Promise<void>((resolve) => {
|
||||
const checkReadyState = () => {
|
||||
if (document.readyState === 'complete') {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkReadyState, 100);
|
||||
}
|
||||
};
|
||||
checkReadyState();
|
||||
});
|
||||
// // 4. 等待页面稳定(document.readyState === 'complete')
|
||||
// await new Promise<void>((resolve) => {
|
||||
// const checkReadyState = () => {
|
||||
// if (document.readyState === 'complete') {
|
||||
// resolve();
|
||||
// } else {
|
||||
// setTimeout(checkReadyState, 100);
|
||||
// }
|
||||
// };
|
||||
// checkReadyState();
|
||||
// });
|
||||
|
||||
// 5. 额外等待确保内容加载
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
// // 5. 额外等待确保内容加载
|
||||
// await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
return true;
|
||||
}
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
// return false;
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
export class AmazonDetailPageInjector extends BaseInjector {
|
||||
|
||||
@ -5,7 +5,14 @@ import { usePageWorker } from '~/page-worker';
|
||||
import MulInputModal from '~/sidepanel/views/components/MulInputModal.vue';
|
||||
const message = useMessage();
|
||||
const showModal = ref(false);
|
||||
const initKeysCount = ref(0);
|
||||
|
||||
const initKeysCount = ref(keywordsList.value.length);
|
||||
const currentIndex = computed(() => {
|
||||
if (timelines.value.length === 0) return null;
|
||||
if (keywordsList.value.length === 0) return null;
|
||||
return initKeysCount.value - keywordsList.value.length + 1;
|
||||
});
|
||||
const isRestarting = ref(false);
|
||||
//#region Initial Page Worker
|
||||
const worker = usePageWorker('amazon', { objects: ['search'] });
|
||||
worker.on('error', ({ message: msg }) => {
|
||||
@ -31,6 +38,8 @@ const timelines = ref<Timeline[]>([]);
|
||||
|
||||
const launch = async () => {
|
||||
const kws = unref(keywordsList);
|
||||
console.log(isRestarting.value);
|
||||
|
||||
timelines.value = [
|
||||
{
|
||||
type: 'info',
|
||||
@ -40,7 +49,7 @@ const launch = async () => {
|
||||
},
|
||||
];
|
||||
// timelines.value.push();
|
||||
initKeysCount.value = kws.length;
|
||||
// initKeysCount.value = kws.length;
|
||||
await worker.runSearchPageTask(kws, {
|
||||
progress: (remains) => {
|
||||
if (remains.length > 0) {
|
||||
@ -60,6 +69,23 @@ const launch = async () => {
|
||||
time: new Date().toLocaleString(),
|
||||
content: `搜索任务结束`,
|
||||
});
|
||||
|
||||
if (keywordsList.value.length === 0) {
|
||||
message.success('所有关键词数据采集完成!');
|
||||
} else {
|
||||
if (!isRestarting.value) return;
|
||||
console.info('5秒后重新采集!');
|
||||
message.info('5秒后重新采集!');
|
||||
timelines.value.push({
|
||||
type: 'info',
|
||||
title: '重新采集',
|
||||
time: new Date().toLocaleString(),
|
||||
content: `5秒后重新采集!`,
|
||||
});
|
||||
setTimeout(() => {
|
||||
handleStart();
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStart = async () => {
|
||||
@ -80,6 +106,7 @@ const clickInputButton = (e: MouseEvent) => {
|
||||
|
||||
const handleModalConfirm = (keys: string[]) => {
|
||||
keywordsList.value = keys;
|
||||
initKeysCount.value = keys.length;
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -92,11 +119,11 @@ const handleModalConfirm = (keys: string[]) => {
|
||||
<span style="display: inline-block"> 当前剩余关键词数量: {{ keywordsList.length }} 条</span>
|
||||
<span style="display: inline-block">
|
||||
现在爬到第
|
||||
{{
|
||||
initKeysCount - keywordsList.length + 1 ? '---' : initKeysCount - keywordsList.length + 1
|
||||
}}
|
||||
条</span
|
||||
{{ currentIndex ?? '--' }} 条</span
|
||||
>
|
||||
<span style="display: inline-block">
|
||||
是否自动重新采集: <n-switch v-model:value="isRestarting" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="interactive-section">
|
||||
<n-infinite-scroll style="max-height: 40vh; padding-right: 16px" :distance="10">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||
|
||||
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);
|
||||
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', []);
|
||||
|
||||
export const detailAsinInput = useWebExtensionStorage<string>('detailAsinInputText', '');
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user