1
This commit is contained in:
197
mv2_simple_crx/src/actions/amazon.js
Normal file
197
mv2_simple_crx/src/actions/amazon.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// amazon_top_list:Amazon TOP 榜单抓取(Best Sellers / New Releases / Movers & Shakers)
|
||||||
|
|
||||||
|
import { openTab } from '../libs/tabs.js';
|
||||||
|
import { execute_script } from '../libs/inject.js';
|
||||||
|
|
||||||
|
export function amazon_search_list(data, sendResponse) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const category_keyword = (data && data.category_keyword) ? String(data.category_keyword).trim() : '';
|
||||||
|
|
||||||
|
const keyword = category_keyword || '野餐包';
|
||||||
|
|
||||||
|
// 用你给的 URL 作为模板,只替换 k 参数,其它参数保持一致
|
||||||
|
const default_url = (() => {
|
||||||
|
const u = new URL('https://www.amazon.com/s');
|
||||||
|
u.searchParams.set('k', keyword);
|
||||||
|
u.searchParams.set('__mk_zh_CN', '亚马逊网站');
|
||||||
|
u.searchParams.set('crid', 'ZKNCI4U8BBAP');
|
||||||
|
u.searchParams.set('sprefix', '野餐bao,caps,388');
|
||||||
|
u.searchParams.set('ref', 'nb_sb_noss');
|
||||||
|
return u.toString();
|
||||||
|
})();
|
||||||
|
|
||||||
|
const url = (data && data.url) ? String(data.url).trim() : default_url;
|
||||||
|
|
||||||
|
let times = 0;
|
||||||
|
|
||||||
|
openTab({
|
||||||
|
url,
|
||||||
|
latest: false,
|
||||||
|
top: 20,
|
||||||
|
left: 20,
|
||||||
|
width: 1440,
|
||||||
|
height: 900,
|
||||||
|
target: '__amazon_search_list',
|
||||||
|
|
||||||
|
tabError(tab, details) {
|
||||||
|
const result = {
|
||||||
|
code: 30,
|
||||||
|
status: false,
|
||||||
|
message: (details && (details.message || details.statusLine || details.error)) || 'tab error',
|
||||||
|
data: null,
|
||||||
|
documentURI: details && details.url,
|
||||||
|
};
|
||||||
|
|
||||||
|
sendResponse && sendResponse({ action: 'amazon_search_list', data: result });
|
||||||
|
sendResponse && sendResponse.log && sendResponse.log(result);
|
||||||
|
|
||||||
|
if (tab && tab.remove) {
|
||||||
|
tab.remove(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new Error(result.message));
|
||||||
|
},
|
||||||
|
|
||||||
|
tabUpdated(tab, details) {
|
||||||
|
if (times > 0) return;
|
||||||
|
times += 1;
|
||||||
|
|
||||||
|
const tab_id = tab && tab.id;
|
||||||
|
if (!tab_id) {
|
||||||
|
return reject(new Error('tab.id 为空'));
|
||||||
|
}
|
||||||
|
|
||||||
|
execute_script(
|
||||||
|
tab_id,
|
||||||
|
{
|
||||||
|
fn: (params) => {
|
||||||
|
const action = 'amazon_search_list';
|
||||||
|
const start_url = params && params.url ? String(params.url) : location.href;
|
||||||
|
const category_keyword = params && params.category_keyword ? String(params.category_keyword).trim() : '';
|
||||||
|
|
||||||
|
function send(action_name, data) {
|
||||||
|
try {
|
||||||
|
chrome.runtime.sendMessage({ type: 'push', action: action_name, data });
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function abs_url(href) {
|
||||||
|
try {
|
||||||
|
return new URL(href, location.origin).toString();
|
||||||
|
} catch (_) {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_asin_from_url(url) {
|
||||||
|
if (!url || typeof url !== 'string') return null;
|
||||||
|
const m = url.match(/\/dp\/([A-Z0-9]{10})/i) || url.match(/\/gp\/product\/([A-Z0-9]{10})/i);
|
||||||
|
return m ? m[1].toUpperCase() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extract_results() {
|
||||||
|
const items = [];
|
||||||
|
const nodes = document.querySelectorAll('div.s-main-slot div[data-component-type="s-search-result"]');
|
||||||
|
nodes.forEach((el, idx) => {
|
||||||
|
const asin = (el.getAttribute('data-asin') || '').trim() || null;
|
||||||
|
|
||||||
|
const title_el = el.querySelector('h2 span') || el.querySelector('h2');
|
||||||
|
const title = title_el ? title_el.textContent.trim() : null;
|
||||||
|
|
||||||
|
// 搜索页有的标题在 h2 里,不一定包在 <a> 里;链接一般在 a[href*="/dp/"]
|
||||||
|
const a = el.querySelector('a[href*="/dp/"], a[href*="/gp/product/"]');
|
||||||
|
const href = a ? a.getAttribute('href') : null;
|
||||||
|
const url = href ? abs_url(href) : null;
|
||||||
|
|
||||||
|
|
||||||
|
const price_el = el.querySelector('span.a-price > span.a-offscreen');
|
||||||
|
const price = price_el ? price_el.textContent.trim() : null;
|
||||||
|
|
||||||
|
const rating_el = el.querySelector('span.a-icon-alt');
|
||||||
|
const rating_text = rating_el ? rating_el.textContent.trim() : null;
|
||||||
|
|
||||||
|
// 评论数在不同语言/布局下 class 不稳定,这里做弱依赖
|
||||||
|
const review_count_el =
|
||||||
|
el.querySelector('span[aria-label$="ratings"]') ||
|
||||||
|
el.querySelector('span[aria-label$="rating"]') ||
|
||||||
|
el.querySelector('span[aria-label$="评价"]') ||
|
||||||
|
el.querySelector('span[aria-label$="评分"]');
|
||||||
|
const review_count_text = review_count_el ? review_count_el.textContent.trim() : null;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
index: idx + 1,
|
||||||
|
asin: asin || parse_asin_from_url(url),
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
price,
|
||||||
|
rating_text,
|
||||||
|
review_count_text,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = extract_results();
|
||||||
|
|
||||||
|
send(action, {
|
||||||
|
stage: 'start',
|
||||||
|
start_url,
|
||||||
|
href: location.href,
|
||||||
|
category_keyword,
|
||||||
|
total: items.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 不逐条推送,直接一次性推完整列表
|
||||||
|
send(action, {
|
||||||
|
stage: 'list',
|
||||||
|
category_keyword,
|
||||||
|
total: items.length,
|
||||||
|
items,
|
||||||
|
});
|
||||||
|
|
||||||
|
send(action, { stage: 'end', category_keyword, total: items.length });
|
||||||
|
},
|
||||||
|
args: [{ url, category_keyword }],
|
||||||
|
},
|
||||||
|
'document_idle',
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
const result = {
|
||||||
|
code: 0,
|
||||||
|
status: true,
|
||||||
|
message: 'ok',
|
||||||
|
data: { tab_id, url, category_keyword },
|
||||||
|
};
|
||||||
|
sendResponse && sendResponse({ action: 'amazon_search_list', data: result });
|
||||||
|
sendResponse && sendResponse.log && sendResponse.log(result);
|
||||||
|
resolve({ tab_id, url, category_keyword });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (tab && tab.remove) {
|
||||||
|
tab.remove(500);
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
amazon_search_list.desc = 'Amazon 搜索结果列表抓取(DOM 解析)';
|
||||||
|
amazon_search_list.params = {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
desc: '可选,传入完整搜索 URL(不传则按 category_keyword 生成)',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
category_keyword: {
|
||||||
|
type: 'string',
|
||||||
|
desc: '分类关键词',
|
||||||
|
default: '野餐包',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
// zhipu_query_position_page:按你原项目的回调风格实现(参考 @zhipu.js 15-29)
|
|
||||||
|
|
||||||
import { openTab } from '../libs/tabs.js';
|
|
||||||
import { execute_script } from '../libs/inject.js';
|
|
||||||
|
|
||||||
function injected_zhipu_query_position_page() {
|
|
||||||
// 运行在 content 隔离环境:监听页面派发的 __REQUEST_DONE
|
|
||||||
// 命中接口后用 chrome.runtime.sendMessage 回传给 background
|
|
||||||
const action = 'zhipu_query_position_page';
|
|
||||||
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise((r) => setTimeout(r, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(action_name, data) {
|
|
||||||
try {
|
|
||||||
chrome.runtime.sendMessage({ type: 'push', action: action_name, data });
|
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!window.__zhipu_job_posts_listener__) {
|
|
||||||
window.__zhipu_job_posts_listener__ = true;
|
|
||||||
window.addEventListener('__REQUEST_DONE', (event) => {
|
|
||||||
|
|
||||||
const detail = event && event.detail ? event.detail : {};
|
|
||||||
const url = detail.url || '';
|
|
||||||
if (typeof url === 'string' && url.includes('/api/v1/search/job/posts')) {
|
|
||||||
send('zhipu_job_posts', detail.json);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function auto_flip_pages() {
|
|
||||||
await sleep(1500);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const next_li = document.querySelector('.atsx-pagination-next');
|
|
||||||
if (!next_li) {
|
|
||||||
send(action, { is_end: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const disabled =
|
|
||||||
next_li.getAttribute('aria-disabled') === 'true' ||
|
|
||||||
next_li.classList.contains('atsx-pagination-disabled');
|
|
||||||
|
|
||||||
if (disabled) {
|
|
||||||
send(action, { is_end: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next_btn = next_li.querySelector('.atsx-pagination-item-link');
|
|
||||||
if (!next_btn) {
|
|
||||||
send(action, { is_end: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
next_btn.click();
|
|
||||||
await sleep(2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!location.href.startsWith('https://zhipu-ai.jobs.feishu.cn/referral/position')) {
|
|
||||||
send(action, { error: '未定位到智谱职位列表页', url: location.href });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto_flip_pages().catch((err) => {
|
|
||||||
send(action, { error: (err && err.message) || String(err) });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function zhipu_query_position_page(data, sendResponse) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const url = 'https://zhipu-ai.jobs.feishu.cn/referral/position';
|
|
||||||
|
|
||||||
let times = 0;
|
|
||||||
|
|
||||||
openTab({
|
|
||||||
url,
|
|
||||||
latest: false,
|
|
||||||
top: 20,
|
|
||||||
left: 20,
|
|
||||||
width: 1440,
|
|
||||||
height: 900,
|
|
||||||
target: '__zhipu_query_position_page',
|
|
||||||
|
|
||||||
tabError(tab, details) {
|
|
||||||
const result = {
|
|
||||||
code: 30,
|
|
||||||
status: false,
|
|
||||||
message: details && (details.message || details.statusLine || details.error) || 'tab error',
|
|
||||||
data: null,
|
|
||||||
documentURI: details && details.url,
|
|
||||||
};
|
|
||||||
|
|
||||||
sendResponse && sendResponse({ action: 'zhipu_query_position_page', data: result });
|
|
||||||
sendResponse && sendResponse.log && sendResponse.log(result);
|
|
||||||
|
|
||||||
if (tab && tab.remove) {
|
|
||||||
tab.remove(1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
reject(new Error(result.message));
|
|
||||||
},
|
|
||||||
|
|
||||||
tabUpdated(tab, details) {
|
|
||||||
if (times > 0) return;
|
|
||||||
times += 1;
|
|
||||||
|
|
||||||
const tab_id = tab && tab.id;
|
|
||||||
if (!tab_id) {
|
|
||||||
return reject(new Error('tab.id 为空'));
|
|
||||||
}
|
|
||||||
|
|
||||||
execute_script(tab_id, injected_zhipu_query_position_page, 'document_idle')
|
|
||||||
.then(() => {
|
|
||||||
const result = { code: 0, status: true, message: 'ok', data: { tab_id, url } };
|
|
||||||
sendResponse && sendResponse({ action: 'zhipu_query_position_page', data: result });
|
|
||||||
sendResponse && sendResponse.log && sendResponse.log(result);
|
|
||||||
|
|
||||||
// 这里不自动关 tab,方便你调试;要关就打开下一行
|
|
||||||
// tab.remove(3500);
|
|
||||||
|
|
||||||
resolve({ tab_id, url });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (tab && tab.remove) {
|
|
||||||
tab.remove(500);
|
|
||||||
}
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,23 @@
|
|||||||
import { zhipu_query_position_page } from '../actions/zhipu.js';
|
|
||||||
|
import { amazon_search_list } from '../actions/amazon.js';
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
zhipu_query_position_page,
|
amazon_search_list,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function list_actions_meta() {
|
||||||
|
const meta = {};
|
||||||
|
Object.keys(actions).forEach((name) => {
|
||||||
|
const fn = actions[name];
|
||||||
|
meta[name] = {
|
||||||
|
name,
|
||||||
|
desc: fn && fn.desc ? fn.desc : '',
|
||||||
|
params: fn && fn.params ? fn.params : {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
function create_action_send_response(sender) {
|
function create_action_send_response(sender) {
|
||||||
const fn = (payload) => {
|
const fn = (payload) => {
|
||||||
emit_ui_event('push', { type: 'reply', ...payload, sender });
|
emit_ui_event('push', { type: 'reply', ...payload, sender });
|
||||||
@@ -61,6 +75,21 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UI 获取 action 元信息(用于下拉/默认参数)
|
||||||
|
if (message.action === 'meta_actions') {
|
||||||
|
sendResponse({ ok: true, data: list_actions_meta() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI 刷新后台(重启 background page)
|
||||||
|
if (message.action === 'reload_background') {
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
setTimeout(() => {
|
||||||
|
location.reload();
|
||||||
|
}, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fn = actions[message.action];
|
const fn = actions[message.action];
|
||||||
if (!fn) {
|
if (!fn) {
|
||||||
sendResponse({ ok: false, error: '未知 action: ' + message.action });
|
sendResponse({ ok: false, error: '未知 action: ' + message.action });
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
// executeScript:MV2 使用 chrome.tabs.executeScript
|
// executeScript:MV2 使用 chrome.tabs.executeScript
|
||||||
|
|
||||||
function normalize_code(code) {
|
function normalize_code(code) {
|
||||||
|
// 支持:直接传函数
|
||||||
if (typeof code === 'function') {
|
if (typeof code === 'function') {
|
||||||
return `(${code.toString()})();`;
|
return `(${code.toString()})();`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 支持:传 { fn, args },这样 action 文件里不需要拼字符串也能传参
|
||||||
|
if (code && typeof code === 'object' && typeof code.fn === 'function') {
|
||||||
|
const args = Array.isArray(code.args) ? code.args : [];
|
||||||
|
return `(${code.fn.toString()}).apply(null, ${JSON.stringify(args)});`;
|
||||||
|
}
|
||||||
|
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
<label class="label">方法名(action)</label>
|
<label class="label">方法名(action)</label>
|
||||||
<select id="action_name" class="input">
|
<select id="action_name" class="input">
|
||||||
<option value="zhipu_query_position_page">zhipu_query_position_page</option>
|
<option value="zhipu_query_position_page">zhipu_query_position_page</option>
|
||||||
|
<option value="amazon_top_list">amazon_top_list</option>
|
||||||
|
<option value="amazon_search_list">amazon_search_list</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label class="label">参数(JSON)</label>
|
<label class="label">参数(JSON)</label>
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="btn_run" class="btn primary">执行</button>
|
<button id="btn_run" class="btn primary">执行</button>
|
||||||
<button id="btn_clear" class="btn">清空日志</button>
|
<button id="btn_clear" class="btn">清空日志</button>
|
||||||
|
<button id="btn_bg_reload" class="btn">刷新后台</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hint">
|
<div class="hint">
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ const action_name_el = document.getElementById('action_name');
|
|||||||
const action_params_el = document.getElementById('action_params');
|
const action_params_el = document.getElementById('action_params');
|
||||||
const btn_run_el = document.getElementById('btn_run');
|
const btn_run_el = document.getElementById('btn_run');
|
||||||
const btn_clear_el = document.getElementById('btn_clear');
|
const btn_clear_el = document.getElementById('btn_clear');
|
||||||
|
const btn_bg_reload_el = document.getElementById('btn_bg_reload');
|
||||||
const last_response_el = document.getElementById('last_response');
|
const last_response_el = document.getElementById('last_response');
|
||||||
const event_log_el = document.getElementById('event_log');
|
const event_log_el = document.getElementById('event_log');
|
||||||
|
let actions_meta = {};
|
||||||
|
|
||||||
function now_time() {
|
function now_time() {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
@@ -47,6 +49,53 @@ btn_clear_el.addEventListener('click', () => {
|
|||||||
event_log_el.textContent = '';
|
event_log_el.textContent = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btn_bg_reload_el.addEventListener('click', () => {
|
||||||
|
append_event_line({ type: 'ui', action: 'reload_background' });
|
||||||
|
chrome.runtime.sendMessage({ action: 'reload_background', data: {} }, (res) => {
|
||||||
|
append_event_line({ type: 'ui', action: 'reload_background_done', res });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function build_default_params(params_schema) {
|
||||||
|
const obj = {};
|
||||||
|
if (!params_schema || typeof params_schema !== 'object') return obj;
|
||||||
|
Object.keys(params_schema).forEach((k) => {
|
||||||
|
const item = params_schema[k];
|
||||||
|
if (item && typeof item === 'object' && Object.prototype.hasOwnProperty.call(item, 'default')) {
|
||||||
|
obj[k] = item.default;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh_params_editor() {
|
||||||
|
const name = action_name_el.value;
|
||||||
|
const meta = actions_meta[name];
|
||||||
|
if (!meta) return;
|
||||||
|
const defaults = build_default_params(meta.params);
|
||||||
|
action_params_el.value = JSON.stringify(defaults, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh_action_select() {
|
||||||
|
const current = action_name_el.value;
|
||||||
|
action_name_el.innerHTML = '';
|
||||||
|
Object.keys(actions_meta).forEach((name) => {
|
||||||
|
const meta = actions_meta[name];
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = name;
|
||||||
|
opt.textContent = meta && meta.desc ? `${name} - ${meta.desc}` : name;
|
||||||
|
action_name_el.appendChild(opt);
|
||||||
|
});
|
||||||
|
if (current && actions_meta[current]) {
|
||||||
|
action_name_el.value = current;
|
||||||
|
}
|
||||||
|
refresh_params_editor();
|
||||||
|
}
|
||||||
|
|
||||||
|
action_name_el.addEventListener('change', () => {
|
||||||
|
refresh_params_editor();
|
||||||
|
});
|
||||||
|
|
||||||
// background -> UI 的推送
|
// background -> UI 的推送
|
||||||
chrome.runtime.onMessage.addListener((message) => {
|
chrome.runtime.onMessage.addListener((message) => {
|
||||||
if (!message || message.channel !== 'ui_event') {
|
if (!message || message.channel !== 'ui_event') {
|
||||||
@@ -69,4 +118,12 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始化:拉取 action 元信息,生成下拉 + 默认参数
|
||||||
|
chrome.runtime.sendMessage({ action: 'meta_actions', data: {} }, (res) => {
|
||||||
|
if (res && res.ok && res.data) {
|
||||||
|
actions_meta = res.data;
|
||||||
|
refresh_action_select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
append_event_line({ type: 'ready', hint: '点击右侧/上方执行按钮开始' });
|
append_event_line({ type: 'ready', hint: '点击右侧/上方执行按钮开始' });
|
||||||
|
|||||||
Reference in New Issue
Block a user