This commit is contained in:
张成
2026-03-17 18:21:17 +08:00
parent 99748a59bf
commit f5880baacf
6 changed files with 220 additions and 124 deletions

View File

@@ -39,14 +39,20 @@ export function amazon_search_list(data, sendResponse) {
try {
const tab = await tab_task.open_async();
await tab.execute_script(injected_amazon_search_list, [{ url, category_keyword }], 'document_idle');
const injected_result_list = await tab.execute_script(injected_amazon_search_list, [{ url, category_keyword }], 'document_idle');
const injected_result = Array.isArray(injected_result_list) ? injected_result_list[0] : null;
const result = { code: 0, status: true, message: 'ok', data: { tab_id: tab.id, url, category_keyword } };
const result = {
code: 0,
status: true,
message: 'ok',
data: { tab_id: tab.id, url, category_keyword, result: injected_result },
};
send_action('amazon_search_list', result);
resolve({ tab_id: tab.id, url, category_keyword });
resolve({ tab_id: tab.id, url, category_keyword, result: injected_result });
// 可选:自动关 tab默认不关方便调试
// tab && tab.remove && tab.remove(3500);
// 成功后关闭打开的 tab同时会关闭 popup window
tab.remove(0);
} catch (err) {
const result = {
code: 30,

View File

@@ -1,18 +1,9 @@
// 注入到页面的 Amazon 搜索列表解析逻辑
export function injected_amazon_search_list(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();
@@ -69,7 +60,12 @@ export function injected_amazon_search_list(params) {
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 });
// 只返回一次结果,避免 send 太多影响判定
return {
start_url,
href: location.href,
category_keyword,
total: items.length,
items,
};
}

View File

@@ -16,6 +16,15 @@ function attach_tab_helpers(tab) {
return await execute_script(tab.id, fn, args, run_at);
};
tab.close_window = function close_window(delay_ms) {
delay_ms = Number.isFinite(delay_ms) ? delay_ms : 0;
setTimeout(() => {
if (tab.windowId) {
chrome.windows.remove(tab.windowId, () => void 0);
}
}, Math.max(0, delay_ms));
};
return tab;
}
@@ -96,9 +105,48 @@ export function create_tab_task(url) {
return this;
},
async open_async() {
// 直接返回 tab 对象(带 remove / execute_script
const { tab } = await open_tab(this.url, { active: this.active !== false });
return tab;
// 用 chrome.windows.create 新开窗口承载 tab
const win = await new Promise((resolve, reject) => {
chrome.windows.create(
{
url: 'about:blank',
type: 'popup',
focused: true,
top: this.top,
left: this.left,
width: this.width,
height: this.height,
},
(w) => {
if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
resolve(w);
},
);
});
const tab0 = win && win.tabs && win.tabs[0] ? win.tabs[0] : null;
if (!tab0 || !tab0.id) {
throw new Error('popup window 创建失败');
}
await new Promise((resolve, reject) => {
chrome.tabs.update(tab0.id, { url: this.url, active: this.active !== false }, () => {
if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
resolve(true);
});
});
const tab_done = await new Promise((resolve) => {
const on_updated = (tab_id, change_info, tab) => {
if (tab_id !== tab0.id) return;
if (change_info.status !== 'complete') return;
chrome.tabs.onUpdated.removeListener(on_updated);
resolve(tab);
};
chrome.tabs.onUpdated.addListener(on_updated);
});
return attach_tab_helpers(tab_done);
},
};

View File

@@ -1,11 +1,9 @@
:root {
--bg: #0b1220;
--card: #121b2d;
--card2: #0f1728;
--text: #e7eefc;
--muted: #9db0d1;
--border: rgba(255, 255, 255, 0.08);
--primary: #4f8cff;
--bg: #ffffff;
--text: #111111;
--muted: #666666;
--border: #e5e7eb;
--primary: #2563eb;
}
* { box-sizing: border-box; }
@@ -13,16 +11,19 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
background: radial-gradient(1200px 700px at 20% 10%, rgba(79, 140, 255, 0.25), transparent 60%),
radial-gradient(900px 600px at 90% 20%, rgba(167, 139, 250, 0.18), transparent 55%),
var(--bg);
background: var(--bg);
color: var(--text);
height: 100vh;
}
.app {
max-width: 1200px;
margin: 0 auto;
max-width: none;
margin: 0;
padding: 18px;
height: 100vh;
display: flex;
flex-direction: column;
gap: 10px;
}
.header {
@@ -42,28 +43,37 @@ body {
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 300px 1fr;
gap: 14px;
flex: 1;
min-height: 0;
}
.card {
border: 1px solid var(--border);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02));
backdrop-filter: blur(8px);
border-radius: 12px;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
.card_title {
padding: 12px 14px;
border-bottom: 1px solid var(--border);
background: rgba(0, 0, 0, 0.15);
background: #f9fafb;
font-weight: 600;
font-size: 13px;
}
.form {
padding: 14px;
display: flex;
flex-direction: column;
gap: 0;
flex: 1;
min-height: 0;
}
.label {
@@ -77,19 +87,20 @@ body {
.textarea {
width: 100%;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.25);
background: #ffffff;
color: var(--text);
border-radius: 10px;
border-radius: 6px;
padding: 10px 10px;
outline: none;
}
.textarea {
min-height: 160px;
min-height: 220px;
resize: vertical;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.5;
flex: 1;
}
.row {
@@ -100,21 +111,22 @@ body {
.btn {
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.25);
background: #ffffff;
color: var(--text);
border-radius: 10px;
border-radius: 6px;
padding: 10px 12px;
cursor: pointer;
font-weight: 600;
}
.btn.primary {
border-color: rgba(79, 140, 255, 0.45);
background: rgba(79, 140, 255, 0.18);
border-color: var(--primary);
color: #ffffff;
background: var(--primary);
}
.btn:hover {
border-color: rgba(255, 255, 255, 0.18);
border-color: #cbd5e1;
}
.hint {
@@ -125,18 +137,26 @@ body {
}
.hint code {
color: #c7d2fe;
color: #111111;
}
.pre {
margin: 0;
padding: 14px;
background: rgba(0, 0, 0, 0.22);
background: #ffffff;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.5;
min-height: 260px;
color: #dbeafe;
min-height: 0;
color: #111111;
flex: 1;
overflow: auto;
}
.pre_small {
flex: 0 0 180px;
max-height: 180px;
overflow: auto;
}
.pre_scroll {

View File

@@ -1,52 +1,59 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>mv2_simple_crx 控制台</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div class="app">
<div class="header">
<div class="title">mv2_simple_crx 控制台</div>
<div class="sub">选择 action填参数执行并查看响应/推送事件</div>
</div>
<div class="grid">
<div class="card">
<div class="card_title">调用</div>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>mv2_simple_crx 控制台</title>
<link rel="stylesheet" href="index.css" />
</head>
<div class="form">
<label class="label">方法名action</label>
<select id="action_name" class="input">
<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>
<label class="label">参数JSON</label>
<textarea id="action_params" class="textarea" spellcheck="false">{}</textarea>
<div class="row">
<button id="btn_run" class="btn primary">执行</button>
<button id="btn_clear" class="btn">清空日志</button>
<button id="btn_bg_reload" class="btn">刷新后台</button>
</div>
<div class="hint">
说明:执行后会显示「响应」,同时如果页面内监听到 <code>__REQUEST_DONE</code> 命中接口,会在「推送事件」里持续追加。
</div>
</div>
</div>
<div class="card">
<div class="card_title">响应</div>
<pre id="last_response" class="pre"></pre>
</div>
</div>
<body>
<div class="app">
<div class="header">
<div class="title">mv2_simple_crx 控制台</div>
<div class="sub">选择 action填参数执行并查看响应/推送事件</div>
</div>
<script src="index.js"></script>
</body>
<div class="grid">
<div class="card">
<div class="card_title">调用</div>
<div class="form">
<label class="label">方法名action</label>
<select id="action_name" class="input">
<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>
<label class="label">参数JSON</label>
<textarea id="action_params" class="textarea" spellcheck="false">{}</textarea>
<div class="row">
<button id="btn_run" class="btn primary">执行</button>
<button id="btn_clear" class="btn">清空日志</button>
<button id="btn_bg_reload" class="btn">刷新后台</button>
</div>
<div class="hint">
说明:执行后会显示「响应」,同时如果页面内监听到 <code>__REQUEST_DONE</code> 命中接口,会在「推送事件」里持续追加。
</div>
</div>
</div>
<div class="card">
<label class="label">动作日志</label>
<pre id="action_log" class="pre pre_small"></pre>
<div class="card_title">响应</div>
<pre id="last_response" class="pre"></pre>
</div>
</div>
</div>
<script src="index.js"></script>
</body>
</html>

View File

@@ -4,8 +4,9 @@ const btn_run_el = document.getElementById('btn_run');
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 action_log_el = document.getElementById('action_log');
let actions_meta = {};
const ui_state = { events: [] };
const ui_state = { last_result: null, actions: [] };
function now_time() {
const d = new Date();
@@ -21,20 +22,45 @@ function safe_json_parse(text) {
}
}
function set_last_response(obj) {
last_response_el.textContent = JSON.stringify(obj, null, 2);
function pick_main_result(res) {
// 右侧只展示最核心的数据,避免被 ok/request_id 包裹层干扰
if (res && res.ok && res.data) {
// 约定action 返回的核心结果放在 data.result例如 amazon_search_list 的 stage=list
if (res.data.result) return res.data.result;
return res.data;
}
return res;
}
function render_state() {
// 只在一个窗口展示所有数据,方便查看
last_response_el.textContent = JSON.stringify(ui_state, null, 2);
last_response_el.textContent = JSON.stringify(ui_state.last_result, null, 2);
action_log_el.textContent = ui_state.actions.join('\n');
}
function push_event(obj) {
ui_state.events.push({ ts: now_time(), ...obj });
function push_action(obj) {
// 动作日志只保留单行文本,避免 JSON 换行太长
const ts = now_time();
const type = obj && obj.type ? String(obj.type) : 'action';
const action = obj && obj.action ? String(obj.action) : '';
const hint = obj && obj.hint ? String(obj.hint) : '';
const payload = obj && Object.prototype.hasOwnProperty.call(obj, 'payload') ? obj.payload : null;
const res = obj && Object.prototype.hasOwnProperty.call(obj, 'res') ? obj.res : null;
let extra = '';
if (hint) extra = hint;
else if (action) extra = action;
else if (payload !== null) extra = JSON.stringify(payload);
else if (res !== null) extra = JSON.stringify(res);
// 截断避免一行过长
if (extra && extra.length > 180) {
extra = extra.slice(0, 180) + '...';
}
ui_state.actions.push(`[${ts}] ${type}${extra ? ' ' + extra : ''}`);
// 简单限长,避免无限增长
if (ui_state.events.length > 300) {
ui_state.events.splice(0, ui_state.events.length - 300);
if (ui_state.actions.length > 200) {
ui_state.actions.splice(0, ui_state.actions.length - 200);
}
render_state();
}
@@ -43,22 +69,27 @@ btn_run_el.addEventListener('click', () => {
const action = action_name_el.value;
const params = safe_json_parse(action_params_el.value || '{}');
push_event({ type: 'call', action, params });
push_action({ type: 'call', action, params });
ui_state.last_result = { running: true, action, params };
render_state();
chrome.runtime.sendMessage({ action, data: params }, (res) => {
push_event({ type: 'reply', action, res });
push_action({ type: 'reply', action, res });
ui_state.last_result = pick_main_result(res);
render_state();
});
});
btn_clear_el.addEventListener('click', () => {
ui_state.events = [];
ui_state.actions = [];
ui_state.last_result = null;
render_state();
});
btn_bg_reload_el.addEventListener('click', () => {
push_event({ type: 'ui', action: 'reload_background' });
push_action({ type: 'ui', action: 'reload_background' });
chrome.runtime.sendMessage({ action: 'reload_background', data: {} }, (res) => {
push_event({ type: 'ui', action: 'reload_background_done', res });
push_action({ type: 'ui', action: 'reload_background_done', res });
});
});
@@ -108,20 +139,8 @@ chrome.runtime.onMessage.addListener((message) => {
return;
}
if (message.event_name === 'push') {
push_event({ type: 'push', payload: message.payload });
return;
}
if (message.event_name === 'request') {
push_event({ type: 'request', payload: message.payload });
return;
}
if (message.event_name === 'response') {
push_event({ type: 'bg_response', payload: message.payload });
return;
}
// UI 动作都放左侧,不影响右侧主响应
push_action({ type: message.event_name, payload: message.payload });
});
// 初始化:拉取 action 元信息,生成下拉 + 默认参数
@@ -133,4 +152,4 @@ chrome.runtime.sendMessage({ action: 'meta_actions', data: {} }, (res) => {
});
render_state();
push_event({ type: 'ready', hint: '点击右侧/上方执行按钮开始' });
push_action({ type: 'ready', hint: '点击右侧执行按钮开始' });