mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-01-19 13:13:22 +08:00
Update
This commit is contained in:
parent
219b34661a
commit
cbdc3efd22
18
package.json
18
package.json
@ -27,16 +27,14 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/json": "^2.2.356",
|
"@iconify/json": "^2.2.359",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/gulp-terser": "^1.2.6",
|
"@types/node": "^22.16.4",
|
||||||
"@types/node": "^22.16.0",
|
|
||||||
"@types/webextension-polyfill": "^0.12.3",
|
"@types/webextension-polyfill": "^0.12.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vueuse/core": "^13.5.0",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@zumer/snapdom": "^1.9.5",
|
||||||
"@zumer/snapdom": "^1.8.0",
|
|
||||||
"alova": "^3.3.4",
|
"alova": "^3.3.4",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
@ -50,17 +48,19 @@
|
|||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"kolorist": "^1.8.0",
|
"kolorist": "^1.8.0",
|
||||||
"lint-staged": "^16.1.2",
|
"lint-staged": "^16.1.2",
|
||||||
|
"markdown-it-anchor": "^9.2.0",
|
||||||
|
"markdown-it-toc-done-right": "^4.2.0",
|
||||||
"naive-ui": "^2.42.0",
|
"naive-ui": "^2.42.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.6.2",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"sass-embedded": "^1.89.2",
|
"sass-embedded": "^1.89.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"unplugin-auto-import": "^19.3.0",
|
"unplugin-auto-import": "^19.3.0",
|
||||||
"unplugin-icons": "^22.1.0",
|
"unplugin-icons": "^22.1.0",
|
||||||
"unplugin-vue-components": "^28.8.0",
|
"unplugin-vue-components": "^28.8.0",
|
||||||
"vite": "^7.0.2",
|
"unplugin-vue-markdown": "^29.1.0",
|
||||||
"vite-plugin-md": "^0.21.5",
|
"vite": "^7.0.4",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-demi": "^0.14.10",
|
"vue-demi": "^0.14.10",
|
||||||
|
|||||||
1936
pnpm-lock.yaml
generated
1936
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useElementBounding, useParentElement } from '@vueuse/core';
|
|
||||||
import type { EllipsisProps } from 'naive-ui';
|
import type { EllipsisProps } from 'naive-ui';
|
||||||
|
|
||||||
export type TableColumn =
|
export type TableColumn =
|
||||||
@ -18,17 +17,6 @@ export type TableColumn =
|
|||||||
renderExpand: (row: any) => VNode;
|
renderExpand: (row: any) => VNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parentEl = useParentElement();
|
|
||||||
const currentRootEl = useTemplateRef('result-table');
|
|
||||||
const { height: parentHeight } = useElementBounding(parentEl);
|
|
||||||
const tableHeight = computed(() => {
|
|
||||||
let contentHeight = 0;
|
|
||||||
currentRootEl.value?.childNodes.forEach((element) => {
|
|
||||||
contentHeight += (element as Element).getBoundingClientRect().height;
|
|
||||||
});
|
|
||||||
return ~~Math.max(parentHeight.value, contentHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
records: Record<string, unknown>[];
|
records: Record<string, unknown>[];
|
||||||
@ -58,8 +46,13 @@ function generateUUID() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="result-table" ref="result-table" :style="{ height: `${tableHeight}px` }">
|
<div class="result-table">
|
||||||
<n-card class="result-content-container">
|
<n-card
|
||||||
|
class="result-content-container"
|
||||||
|
ref="card"
|
||||||
|
header-class="result-table-header"
|
||||||
|
content-class="result-table-main-content"
|
||||||
|
>
|
||||||
<template #header><slot name="header" /></template>
|
<template #header><slot name="header" /></template>
|
||||||
<template #header-extra><slot name="header-extra" /></template>
|
<template #header-extra><slot name="header-extra" /></template>
|
||||||
<n-empty v-if="itemView.records.length === 0" size="huge">
|
<n-empty v-if="itemView.records.length === 0" size="huge">
|
||||||
@ -95,6 +88,11 @@ function generateUUID() {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.result-content-container {
|
.result-content-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
:deep(*) {
|
||||||
|
transition-duration: 0ms;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.n-card__content:has(.n-empty)) {
|
:deep(.n-card__content:has(.n-empty)) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
/** 数据转换工具类 */
|
||||||
|
|
||||||
export function flattenObject(obj: Record<string, unknown>) {
|
export function flattenObject(obj: Record<string, unknown>) {
|
||||||
const mappedEnties: [string[], unknown][] = [];
|
const mappedEnties: [string[], unknown][] = [];
|
||||||
const stack: string[][] = Object.keys(obj).map((k) => [k]);
|
const stack: string[][] = Object.keys(obj).map((k) => [k]);
|
||||||
|
|||||||
81
src/logic/crypto.ts
Normal file
81
src/logic/crypto.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/** 数据加密工具类 */
|
||||||
|
|
||||||
|
/** 辅助函数:Base64转ArrayBuffer */
|
||||||
|
function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||||
|
const binaryString = atob(base64);
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 辅助函数:ArrayBuffer转Base64 */
|
||||||
|
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||||
|
let binary = '';
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
for (let i = 0; i < bytes.byteLength; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成RSA密钥对 */
|
||||||
|
export async function generateKeyPair(): Promise<CryptoKeyPair> {
|
||||||
|
return crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'RSASSA-PKCS1-v1_5',
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||||
|
hash: { name: 'SHA-256' },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign', 'verify'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 使用私钥对数据进行签名 */
|
||||||
|
export async function signData(privateKey: CryptoKey, data: string) {
|
||||||
|
// 将数据转换为ArrayBuffer
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const encodedData = encoder.encode(data);
|
||||||
|
|
||||||
|
// 使用私钥签名
|
||||||
|
const signature = await crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: 'RSASSA-PKCS1-v1_5',
|
||||||
|
},
|
||||||
|
privateKey,
|
||||||
|
encodedData,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 将签名转换为Base64以便传输
|
||||||
|
return arrayBufferToBase64(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 验证签名 */
|
||||||
|
export async function verifySignature(
|
||||||
|
publicKey: CryptoKey,
|
||||||
|
signature: string,
|
||||||
|
data: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const encodedData: BufferSource = encoder.encode(data);
|
||||||
|
const signatureBuffer: ArrayBuffer = base64ToArrayBuffer(signature);
|
||||||
|
|
||||||
|
return await crypto.subtle.verify(
|
||||||
|
{
|
||||||
|
name: 'RSASSA-PKCS1-v1_5',
|
||||||
|
},
|
||||||
|
publicKey,
|
||||||
|
signatureBuffer,
|
||||||
|
encodedData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**导出秘钥为Base64 */
|
||||||
|
export async function exportKey(key: CryptoKey): Promise<string> {
|
||||||
|
const exportedPublicKey = await crypto.subtle.exportKey('spki', key);
|
||||||
|
const publicKeyBase64 = arrayBufferToBase64(exportedPublicKey);
|
||||||
|
return publicKeyBase64;
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
/** Excel工具类 */
|
||||||
import excel from 'exceljs';
|
import excel from 'exceljs';
|
||||||
|
|
||||||
class Worksheet {
|
class Worksheet {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
/** 内容脚本注入工具类 */
|
||||||
|
|
||||||
type ExecOptions = {
|
type ExecOptions = {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/** 上传图片工具类 */
|
||||||
import { remoteHost } from '~/env';
|
import { remoteHost } from '~/env';
|
||||||
|
|
||||||
export async function uploadImage(
|
export async function uploadImage(
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
[[toc]]
|
||||||
|
|
||||||
# 软件目的
|
# 软件目的
|
||||||
|
|
||||||
本软件的开发旨在自动采集Amazon电商平台的数据,并提供导出和预览数据的功能。
|
本软件的开发旨在自动采集Amazon电商平台的数据,并提供导出和预览数据的功能。
|
||||||
|
|||||||
@ -1,31 +1,41 @@
|
|||||||
import { useLongTask } from '~/composables/useLongTask';
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
import amazon from '../impls/amazon';
|
import amazon from '../impls/amazon';
|
||||||
import { uploadImage } from '~/logic/upload';
|
import { uploadImage } from '~/logic/upload-image';
|
||||||
import { detailItems, reviewItems, searchItems } from '~/storages/amazon';
|
import { detailItems, reviewItems, searchItems } from '~/storages/amazon';
|
||||||
import { createGlobalState } from '@vueuse/core';
|
import { createGlobalState } from '@vueuse/core';
|
||||||
import { useAmazonService } from '~/services/amazon';
|
import { useAmazonService } from '~/services/amazon';
|
||||||
import { LanchTaskBaseOptions } from '../interfaces';
|
import { LanchTaskBaseOptions, WorkerComposable } from '../interfaces/common';
|
||||||
|
import { AmazonPageWorker } from '../interfaces/amazon';
|
||||||
|
|
||||||
|
/** Settings interface for the Amazon page worker */
|
||||||
export interface AmazonPageWorkerSettings {
|
export interface AmazonPageWorkerSettings {
|
||||||
objects?: ('search' | 'detail' | 'review')[];
|
objects?: ('search' | 'detail' | 'review')[];
|
||||||
commitChangeIngerval?: number;
|
commitChangeInterval?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Main function to build the Amazon page worker composable */
|
||||||
function buildAmazonPageWorker() {
|
function buildAmazonPageWorker() {
|
||||||
|
// Reactive settings object
|
||||||
const settings = shallowRef<AmazonPageWorkerSettings>({});
|
const settings = shallowRef<AmazonPageWorkerSettings>({});
|
||||||
|
// Long task management
|
||||||
const { isRunning, startTask } = useLongTask();
|
const { isRunning, startTask } = useLongTask();
|
||||||
|
// Amazon service instance
|
||||||
const service = useAmazonService();
|
const service = useAmazonService();
|
||||||
|
|
||||||
|
// Get the worker instance from implementation
|
||||||
const worker = amazon.getAmazonPageWorker();
|
const worker = amazon.getAmazonPageWorker();
|
||||||
|
|
||||||
|
// Caches for different item types
|
||||||
const searchCache = [] as AmazonSearchItem[];
|
const searchCache = [] as AmazonSearchItem[];
|
||||||
const detailCache = new Map<string, AmazonDetailItem>();
|
const detailCache = new Map<string, AmazonDetailItem>();
|
||||||
const reviewCache = new Map<string, AmazonReview[]>();
|
const reviewCache = new Map<string, AmazonReview[]>();
|
||||||
|
|
||||||
|
// Add search items to cache
|
||||||
const updateSearchCache = (data: AmazonSearchItem[]) => {
|
const updateSearchCache = (data: AmazonSearchItem[]) => {
|
||||||
searchCache.push(...data);
|
searchCache.push(...data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update or add detail item in cache
|
||||||
const updateDetailCache = (data: { asin: string } & Partial<AmazonDetailItem>) => {
|
const updateDetailCache = (data: { asin: string } & Partial<AmazonDetailItem>) => {
|
||||||
const asin = data.asin;
|
const asin = data.asin;
|
||||||
if (detailCache.has(data.asin)) {
|
if (detailCache.has(data.asin)) {
|
||||||
@ -36,7 +46,8 @@ function buildAmazonPageWorker() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateReviews = (data: { asin: string; reviews: AmazonReview[] }) => {
|
// Update or add reviews in cache
|
||||||
|
const updateReviewCache = (data: { asin: string; reviews: AmazonReview[] }) => {
|
||||||
const { asin, reviews } = data;
|
const { asin, reviews } = data;
|
||||||
const values = reviewCache.get(asin) || [];
|
const values = reviewCache.get(asin) || [];
|
||||||
const ids = new Set(values.map((item) => item.id));
|
const ids = new Set(values.map((item) => item.id));
|
||||||
@ -46,13 +57,23 @@ function buildAmazonPageWorker() {
|
|||||||
reviewCache.set(asin, values);
|
reviewCache.set(asin, values);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clear all caches
|
||||||
|
const clearAllCaches = () => {
|
||||||
|
searchCache.splice(0, searchCache.length);
|
||||||
|
detailCache.clear();
|
||||||
|
reviewCache.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Commit cached changes to storage and service
|
||||||
const commitChange = async () => {
|
const commitChange = async () => {
|
||||||
const { objects } = settings.value;
|
const { objects } = settings.value;
|
||||||
|
// Commit search items
|
||||||
if (objects?.includes('search')) {
|
if (objects?.includes('search')) {
|
||||||
searchItems.value = searchItems.value.concat(searchCache);
|
searchItems.value = searchItems.value.concat(searchCache);
|
||||||
await service.commitSearchItems(searchCache);
|
await service.commitSearchItems(searchCache);
|
||||||
searchCache.splice(0, searchCache.length);
|
searchCache.splice(0, searchCache.length);
|
||||||
}
|
}
|
||||||
|
// Commit detail items
|
||||||
if (objects?.includes('detail')) {
|
if (objects?.includes('detail')) {
|
||||||
for (const [k, v] of detailCache.entries()) {
|
for (const [k, v] of detailCache.entries()) {
|
||||||
if (detailItems.value.has(k)) {
|
if (detailItems.value.has(k)) {
|
||||||
@ -65,6 +86,7 @@ function buildAmazonPageWorker() {
|
|||||||
await service.commitDetailItems(detailCache);
|
await service.commitDetailItems(detailCache);
|
||||||
detailCache.clear();
|
detailCache.clear();
|
||||||
}
|
}
|
||||||
|
// Commit reviews
|
||||||
if (objects?.includes('review')) {
|
if (objects?.includes('review')) {
|
||||||
for (const [asin, reviews] of reviewCache.entries()) {
|
for (const [asin, reviews] of reviewCache.entries()) {
|
||||||
if (reviewItems.value.has(asin)) {
|
if (reviewItems.value.has(asin)) {
|
||||||
@ -82,54 +104,65 @@ function buildAmazonPageWorker() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsubscribeFuncs = [] as (() => void)[];
|
// Store unsubscribe functions for worker events
|
||||||
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
|
||||||
|
// Register all relevant worker event handlers
|
||||||
|
function registerWorkerEvents() {
|
||||||
|
return [
|
||||||
|
// Stop worker on error
|
||||||
|
worker.on('error', () => {
|
||||||
|
worker.stop();
|
||||||
|
}),
|
||||||
|
// Collect search item links
|
||||||
|
worker.on('item-links-collected', ({ objs }) => {
|
||||||
|
updateSearchCache(objs);
|
||||||
|
}),
|
||||||
|
// Collect base info for detail items
|
||||||
|
worker.on('item-base-info-collected', (ev) => {
|
||||||
|
updateDetailCache(ev);
|
||||||
|
}),
|
||||||
|
// Collect category rank for detail items
|
||||||
|
worker.on('item-category-rank-collected', (ev) => {
|
||||||
|
updateDetailCache(ev);
|
||||||
|
}),
|
||||||
|
// Collect images for detail items
|
||||||
|
worker.on('item-images-collected', (ev) => {
|
||||||
|
updateDetailCache(ev);
|
||||||
|
}),
|
||||||
|
// Collect A+ content screenshots and upload
|
||||||
|
worker.on('item-aplus-screenshot-collect', async (ev) => {
|
||||||
|
const url = await uploadImage(ev.base64data, `${ev.asin}.png`);
|
||||||
|
url && updateDetailCache({ asin: ev.asin, aplus: url });
|
||||||
|
}),
|
||||||
|
// Collect extra info for detail items
|
||||||
|
worker.on('item-extra-info-collect', (ev) => {
|
||||||
|
updateDetailCache({ asin: ev.asin, ...ev.info });
|
||||||
|
}),
|
||||||
|
// Collect reviews
|
||||||
|
worker.on('item-review-collected', (ev) => {
|
||||||
|
updateReviewCache(ev);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register event handlers on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
unsubscribeFuncs.push(
|
unsubscribes.push(...registerWorkerEvents());
|
||||||
...[
|
|
||||||
worker.on('error', () => {
|
|
||||||
worker.stop();
|
|
||||||
}),
|
|
||||||
worker.on('item-links-collected', ({ objs }) => {
|
|
||||||
updateSearchCache(objs);
|
|
||||||
}),
|
|
||||||
worker.on('item-base-info-collected', (ev) => {
|
|
||||||
updateDetailCache(ev);
|
|
||||||
}),
|
|
||||||
worker.on('item-category-rank-collected', (ev) => {
|
|
||||||
updateDetailCache(ev);
|
|
||||||
}),
|
|
||||||
worker.on('item-images-collected', (ev) => {
|
|
||||||
updateDetailCache(ev);
|
|
||||||
}),
|
|
||||||
// worker.on('item-top-reviews-collected', (ev) => {
|
|
||||||
// updateDetailCache(ev);
|
|
||||||
// }),
|
|
||||||
worker.on('item-aplus-screenshot-collect', async (ev) => {
|
|
||||||
const url = await uploadImage(ev.base64data, `${ev.asin}.png`);
|
|
||||||
url && updateDetailCache({ asin: ev.asin, aplus: url });
|
|
||||||
}),
|
|
||||||
worker.on('item-extra-info-collect', (ev) => {
|
|
||||||
updateDetailCache({ asin: ev.asin, ...ev.info });
|
|
||||||
}),
|
|
||||||
worker.on('item-review-collected', (ev) => {
|
|
||||||
updateReviews(ev);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Unregister event handlers on unmount
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unsubscribeFuncs.forEach((unsubscribe) => unsubscribe());
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
unsubscribeFuncs.splice(0, unsubscribeFuncs.length);
|
unsubscribes.splice(0, unsubscribes.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**Commit change by interval time */
|
/**
|
||||||
|
* Task wrapper: commit changes by interval during task execution
|
||||||
|
*/
|
||||||
const taskWrapper1 = <T extends (...params: any[]) => Promise<void>>(func: T) => {
|
const taskWrapper1 = <T extends (...params: any[]) => Promise<void>>(func: T) => {
|
||||||
const { commitChangeIngerval = 10000 } = settings.value;
|
const { commitChangeInterval: commitChangeIngerval = 10000 } = settings.value;
|
||||||
searchCache.splice(0, searchCache.length);
|
clearAllCaches();
|
||||||
detailCache.clear();
|
|
||||||
reviewCache.clear();
|
|
||||||
return (...params: Parameters<T>) =>
|
return (...params: Parameters<T>) =>
|
||||||
startTask(async () => {
|
startTask(async () => {
|
||||||
const interval = setInterval(() => commitChange(), commitChangeIngerval);
|
const interval = setInterval(() => commitChange(), commitChangeIngerval);
|
||||||
@ -139,13 +172,13 @@ function buildAmazonPageWorker() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**Commit changes in the end of task unit */
|
/**
|
||||||
|
* Task wrapper: commit changes at the end of each progress step
|
||||||
|
*/
|
||||||
const taskWrapper2 = <T extends (input: any, options?: LanchTaskBaseOptions) => Promise<void>>(
|
const taskWrapper2 = <T extends (input: any, options?: LanchTaskBaseOptions) => Promise<void>>(
|
||||||
func: T,
|
func: T,
|
||||||
) => {
|
) => {
|
||||||
searchCache.splice(0, searchCache.length);
|
clearAllCaches();
|
||||||
detailCache.clear();
|
|
||||||
reviewCache.clear();
|
|
||||||
return (...params: Parameters<T>) =>
|
return (...params: Parameters<T>) =>
|
||||||
startTask(async () => {
|
startTask(async () => {
|
||||||
if (!params?.[1]) {
|
if (!params?.[1]) {
|
||||||
@ -167,10 +200,12 @@ function buildAmazonPageWorker() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Wrapped task runners for each page type
|
||||||
const runSearchPageTask = taskWrapper1(worker.runSearchPageTask.bind(worker));
|
const runSearchPageTask = taskWrapper1(worker.runSearchPageTask.bind(worker));
|
||||||
const runDetailPageTask = taskWrapper2(worker.runDetailPageTask.bind(worker));
|
const runDetailPageTask = taskWrapper2(worker.runDetailPageTask.bind(worker));
|
||||||
const runReviewPageTask = taskWrapper1(worker.runReviewPageTask.bind(worker));
|
const runReviewPageTask = taskWrapper1(worker.runReviewPageTask.bind(worker));
|
||||||
|
|
||||||
|
// Expose API for the composable
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
isRunning,
|
isRunning,
|
||||||
@ -181,7 +216,8 @@ function buildAmazonPageWorker() {
|
|||||||
off: worker.off.bind(worker),
|
off: worker.off.bind(worker),
|
||||||
once: worker.once.bind(worker),
|
once: worker.once.bind(worker),
|
||||||
stop: worker.stop.bind(worker),
|
stop: worker.stop.bind(worker),
|
||||||
};
|
} as WorkerComposable<AmazonPageWorker, AmazonPageWorkerSettings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a global state composable for the Amazon worker */
|
||||||
export const useAmazonWorker = createGlobalState(buildAmazonPageWorker);
|
export const useAmazonWorker = createGlobalState(buildAmazonPageWorker);
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { useLongTask } from '~/composables/useLongTask';
|
|||||||
import { detailItems as homedepotDetailItems } from '~/storages/homedepot';
|
import { detailItems as homedepotDetailItems } from '~/storages/homedepot';
|
||||||
import homedepot from '../impls/homedepot';
|
import homedepot from '../impls/homedepot';
|
||||||
import { createGlobalState } from '@vueuse/core';
|
import { createGlobalState } from '@vueuse/core';
|
||||||
|
import { WorkerComposable } from '../interfaces/common';
|
||||||
|
import { HomedepotWorker } from '../interfaces/homedepot';
|
||||||
|
|
||||||
export interface HomedepotWorkerSettings {
|
export interface HomedepotWorkerSettings {
|
||||||
objects?: 'detail'[];
|
objects?: 'detail'[];
|
||||||
@ -77,7 +79,7 @@ function buildHomedepotWorker() {
|
|||||||
off: worker.off.bind(worker),
|
off: worker.off.bind(worker),
|
||||||
once: worker.once.bind(worker),
|
once: worker.once.bind(worker),
|
||||||
stop: worker.stop.bind(worker),
|
stop: worker.stop.bind(worker),
|
||||||
};
|
} as WorkerComposable<HomedepotWorker, HomedepotWorkerSettings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHomedepotWorker = createGlobalState(buildHomedepotWorker);
|
export const useHomedepotWorker = createGlobalState(buildHomedepotWorker);
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { AmazonPageWorker, AmazonPageWorkerEvents, LanchTaskBaseOptions } from '../interfaces';
|
|
||||||
import type { Tabs } from 'webextension-polyfill';
|
import type { Tabs } from 'webextension-polyfill';
|
||||||
import { withErrorHandling } from '../error-handler';
|
import { withErrorHandling } from '../error-handler';
|
||||||
import {
|
import {
|
||||||
@ -8,6 +7,8 @@ import {
|
|||||||
} from '../web-injectors/amazon';
|
} from '../web-injectors/amazon';
|
||||||
import { isForbiddenUrl } from '~/env';
|
import { isForbiddenUrl } from '~/env';
|
||||||
import { BaseWorker } from './base';
|
import { BaseWorker } from './base';
|
||||||
|
import { AmazonPageWorker, AmazonPageWorkerEvents } from '../interfaces/amazon';
|
||||||
|
import { LanchTaskBaseOptions } from '../interfaces/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AmazonPageWorkerImpl can run on background & sidepanel & popup,
|
* AmazonPageWorkerImpl can run on background & sidepanel & popup,
|
||||||
@ -250,7 +251,7 @@ class AmazonPageWorkerImpl
|
|||||||
asins: string[],
|
asins: string[],
|
||||||
options: LanchTaskBaseOptions & { aplus?: boolean; extra?: boolean } = {},
|
options: LanchTaskBaseOptions & { aplus?: boolean; extra?: boolean } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { progress, aplus = false } = options;
|
const { progress } = options;
|
||||||
const remains = [...asins];
|
const remains = [...asins];
|
||||||
let interrupt = false;
|
let interrupt = false;
|
||||||
const unsubscribe = this.on('interrupt', () => {
|
const unsubscribe = this.on('interrupt', () => {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import type { HomedepotEvents, HomedepotWorker, LanchTaskBaseOptions } from '../interfaces';
|
|
||||||
import { Tabs } from 'webextension-polyfill';
|
import { Tabs } from 'webextension-polyfill';
|
||||||
import { withErrorHandling } from '../error-handler';
|
import { withErrorHandling } from '../error-handler';
|
||||||
import { HomedepotDetailPageInjector } from '../web-injectors/homedepot';
|
import { HomedepotDetailPageInjector } from '../web-injectors/homedepot';
|
||||||
import { BaseWorker } from './base';
|
import { BaseWorker } from './base';
|
||||||
|
import { LanchTaskBaseOptions } from '../interfaces/common';
|
||||||
|
import { HomedepotEvents, HomedepotWorker } from '../interfaces/homedepot';
|
||||||
|
|
||||||
class HomedepotWorkerImpl
|
class HomedepotWorkerImpl
|
||||||
extends BaseWorker<HomedepotEvents & { interrupt: undefined }>
|
extends BaseWorker<HomedepotEvents & { interrupt: undefined }>
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
import type Emittery from 'emittery';
|
import { LanchTaskBaseOptions, Listener } from './common';
|
||||||
|
|
||||||
type Listener<T> = Pick<Emittery<T>, 'on' | 'off' | 'once'>;
|
|
||||||
|
|
||||||
export type LanchTaskBaseOptions = { progress?: (remains: string[]) => Promise<void> | void };
|
|
||||||
|
|
||||||
export interface AmazonPageWorkerEvents {
|
export interface AmazonPageWorkerEvents {
|
||||||
/** The event is fired when worker collected links to items on the Amazon search page. */
|
/** The event is fired when worker collected links to items on the Amazon search page. */
|
||||||
@ -53,14 +49,14 @@ export interface AmazonPageWorkerEvents {
|
|||||||
|
|
||||||
export interface AmazonPageWorker extends Listener<AmazonPageWorkerEvents> {
|
export interface AmazonPageWorker extends Listener<AmazonPageWorkerEvents> {
|
||||||
/**
|
/**
|
||||||
* Browsing goods search page and collect links to those goods.
|
* Browsing item search pages and collect links to those items.
|
||||||
* @param keywordsList - The keywords list to search for on Amazon.
|
* @param keywordsList - The keywords list to search for on Amazon.
|
||||||
* @param options The Options Specify Behaviors.
|
* @param options The Options Specify Behaviors.
|
||||||
*/
|
*/
|
||||||
runSearchPageTask(keywordsList: string[], options?: LanchTaskBaseOptions): Promise<void>;
|
runSearchPageTask(keywordsList: string[], options?: LanchTaskBaseOptions): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browsing goods detail page and collect target information.
|
* Browsing item detail pages and collect target information.
|
||||||
* @param asins Amazon Standard Identification Numbers.
|
* @param asins Amazon Standard Identification Numbers.
|
||||||
* @param options The Options Specify Behaviors.
|
* @param options The Options Specify Behaviors.
|
||||||
*/
|
*/
|
||||||
@ -70,7 +66,7 @@ export interface AmazonPageWorker extends Listener<AmazonPageWorkerEvents> {
|
|||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browsing goods review page and collect target information.
|
* Browsing item review pages and collect target information.
|
||||||
* @param asins Amazon Standard Identification Numbers.
|
* @param asins Amazon Standard Identification Numbers.
|
||||||
* @param options The Options Specify Behaviors.
|
* @param options The Options Specify Behaviors.
|
||||||
*/
|
*/
|
||||||
@ -84,54 +80,3 @@ export interface AmazonPageWorker extends Listener<AmazonPageWorkerEvents> {
|
|||||||
*/
|
*/
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomedepotEvents {
|
|
||||||
/** The event is fired when detail items collect */
|
|
||||||
['detail-item-collected']: { item: HomedepotDetailItem };
|
|
||||||
|
|
||||||
/** The event is fired when reviews collect */
|
|
||||||
['review-collected']: { reviews: HomedepotReview[] };
|
|
||||||
|
|
||||||
/** The event is fired when error occurs. */
|
|
||||||
['error']: { message: string; url?: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HomedepotWorker extends Listener<HomedepotEvents> {
|
|
||||||
/**
|
|
||||||
* Browsing goods detail page and collect target information
|
|
||||||
*/
|
|
||||||
runDetailPageTask(
|
|
||||||
OSMIDs: string[],
|
|
||||||
options?: LanchTaskBaseOptions & { review?: boolean },
|
|
||||||
): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the worker.
|
|
||||||
*/
|
|
||||||
stop(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LowesEvents {
|
|
||||||
/** The event is fired when detail items collect */
|
|
||||||
['detail-item-collected']: { item: LowesDetailItem };
|
|
||||||
|
|
||||||
/** The event is fired when error occurs. */
|
|
||||||
['error']: { message: string; url?: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LowesWorker {
|
|
||||||
/**
|
|
||||||
* The channel for communication with the Lowes page worker.
|
|
||||||
*/
|
|
||||||
readonly channel: Emittery<LowesEvents>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Browsing goods detail page and collect target information
|
|
||||||
*/
|
|
||||||
runDetailPageTask(urls: string[], options?: LanchTaskBaseOptions): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the worker.
|
|
||||||
*/
|
|
||||||
stop(): Promise<void>;
|
|
||||||
}
|
|
||||||
35
src/page-worker/interfaces/common.ts
Normal file
35
src/page-worker/interfaces/common.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import type Emittery from 'emittery';
|
||||||
|
|
||||||
|
export type Listener<T> = Pick<Emittery<T>, 'on' | 'off' | 'once'>;
|
||||||
|
|
||||||
|
export type LanchTaskBaseOptions = { progress?: (remains: string[]) => Promise<void> | void };
|
||||||
|
|
||||||
|
export interface LowesEvents {
|
||||||
|
/** The event is fired when detail items collect */
|
||||||
|
['detail-item-collected']: { item: LowesDetailItem };
|
||||||
|
|
||||||
|
/** The event is fired when error occurs. */
|
||||||
|
['error']: { message: string; url?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LowesWorker {
|
||||||
|
/**
|
||||||
|
* The channel for communication with the Lowes page worker.
|
||||||
|
*/
|
||||||
|
readonly channel: Emittery<LowesEvents>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browsing goods detail page and collect target information
|
||||||
|
*/
|
||||||
|
runDetailPageTask(urls: string[], options?: LanchTaskBaseOptions): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the worker.
|
||||||
|
*/
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkerComposable<Base, S = {}> = Base & {
|
||||||
|
settings: Ref<S>;
|
||||||
|
isRunning: Ref<boolean>;
|
||||||
|
};
|
||||||
27
src/page-worker/interfaces/homedepot.ts
Normal file
27
src/page-worker/interfaces/homedepot.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { LanchTaskBaseOptions, Listener } from './common';
|
||||||
|
|
||||||
|
export interface HomedepotEvents {
|
||||||
|
/** The event is fired when detail items collect */
|
||||||
|
['detail-item-collected']: { item: HomedepotDetailItem };
|
||||||
|
|
||||||
|
/** The event is fired when reviews collect */
|
||||||
|
['review-collected']: { reviews: HomedepotReview[] };
|
||||||
|
|
||||||
|
/** The event is fired when error occurs. */
|
||||||
|
['error']: { message: string; url?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HomedepotWorker extends Listener<HomedepotEvents> {
|
||||||
|
/**
|
||||||
|
* Browsing goods detail page and collect target information
|
||||||
|
*/
|
||||||
|
runDetailPageTask(
|
||||||
|
OSMIDs: string[],
|
||||||
|
options?: LanchTaskBaseOptions & { review?: boolean },
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the worker.
|
||||||
|
*/
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
@ -2,12 +2,25 @@ import { createAlova } from 'alova';
|
|||||||
import adapterFetch from 'alova/fetch';
|
import adapterFetch from 'alova/fetch';
|
||||||
import { remoteHost } from '~/env';
|
import { remoteHost } from '~/env';
|
||||||
import VueHook from 'alova/vue';
|
import VueHook from 'alova/vue';
|
||||||
|
import { exportKey, generateKeyPair, signData } from '~/logic/crypto';
|
||||||
|
|
||||||
|
const keyPairPromise = generateKeyPair();
|
||||||
|
const publicKeyBase64Promise = keyPairPromise.then(async ({ publicKey }) => exportKey(publicKey));
|
||||||
|
|
||||||
const httpClient = createAlova({
|
const httpClient = createAlova({
|
||||||
baseURL: `http://${remoteHost}`,
|
baseURL: `http://${remoteHost}`,
|
||||||
requestAdapter: adapterFetch(),
|
requestAdapter: adapterFetch(),
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
statesHook: VueHook,
|
statesHook: VueHook,
|
||||||
|
beforeRequest: async (method) => {
|
||||||
|
// 生成签名
|
||||||
|
const { privateKey } = await keyPairPromise;
|
||||||
|
const publicKeyBase64Data = await publicKeyBase64Promise;
|
||||||
|
const dataToSign = method.data ? JSON.stringify(method.data) : '';
|
||||||
|
const signature = await signData(privateKey, dataToSign);
|
||||||
|
method.config.headers['x-signature'] = signature;
|
||||||
|
method.config.headers['x-public-key'] = publicKeyBase64Data;
|
||||||
|
},
|
||||||
responded: {
|
responded: {
|
||||||
onSuccess: (response) => response.json(),
|
onSuccess: (response) => response.json(),
|
||||||
onError: (response: Response) => {
|
onError: (response: Response) => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"incremental": false,
|
"incremental": false,
|
||||||
"useDefineForClassFields": false, // Could **not** be true because of vue-router
|
"useDefineForClassFields": false, // Could **not** be true because of the conflict between vue-router and sidepanel
|
||||||
"target": "es2016",
|
"target": "es2016",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"jsxImportSource": "vue",
|
"jsxImportSource": "vue",
|
||||||
|
|||||||
@ -10,9 +10,11 @@ import IconsResolver from 'unplugin-icons/resolver';
|
|||||||
import Components from 'unplugin-vue-components/vite';
|
import Components from 'unplugin-vue-components/vite';
|
||||||
import AutoImport from 'unplugin-auto-import/vite';
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
import Markdown from 'unplugin-vue-markdown/vite';
|
||||||
|
import MarkdownItAnchor from 'markdown-it-anchor';
|
||||||
|
import markdownItTocDoneRight from 'markdown-it-toc-done-right';
|
||||||
import { isDev, outputDir, port, r } from './scripts/utils.js';
|
import { isDev, outputDir, port, r } from './scripts/utils.js';
|
||||||
import packageJson from './package.json';
|
import packageJson from './package.json';
|
||||||
import Markdown from 'vite-plugin-md';
|
|
||||||
|
|
||||||
export const sharedConfig: UserConfig = {
|
export const sharedConfig: UserConfig = {
|
||||||
root: r('src'),
|
root: r('src'),
|
||||||
@ -30,7 +32,12 @@ export const sharedConfig: UserConfig = {
|
|||||||
Vue({
|
Vue({
|
||||||
include: [/\.vue$/, /\.md$/],
|
include: [/\.vue$/, /\.md$/],
|
||||||
}),
|
}),
|
||||||
Markdown(),
|
Markdown({
|
||||||
|
markdownItSetup(md) {
|
||||||
|
md.use(MarkdownItAnchor, {});
|
||||||
|
md.use(markdownItTocDoneRight);
|
||||||
|
},
|
||||||
|
}),
|
||||||
VueJsx(),
|
VueJsx(),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user