feat: enhance waitForPageLoaded

This commit is contained in:
PetrichorFun 2026-01-23 16:03:04 +08:00
parent 6f15a8bcb5
commit cb249dba87
7 changed files with 285 additions and 80 deletions

View File

@ -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",

View File

@ -10,6 +10,8 @@ export function useLongTask() {
try {
result = await task();
} catch (error) {
console.log(error);
console.error('Task failed:', error);
}
isRunning.value = false;

View File

@ -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';

View File

@ -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 },

View File

@ -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 {

View File

@ -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">

View File

@ -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', '');