1
This commit is contained in:
163
mv2_simple_crx/src/README.md
Normal file
163
mv2_simple_crx/src/README.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 模块化重构说明
|
||||||
|
|
||||||
|
## 🎯 重构目标
|
||||||
|
|
||||||
|
使用现代 ES6 模块化方式,将分散的独立函数整合到统一的导出文件中,提供更友好的开发体验。
|
||||||
|
|
||||||
|
## 📁 新的项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── libs/
|
||||||
|
│ ├── index.js # 统一导出所有库函数
|
||||||
|
│ ├── action_response.js
|
||||||
|
│ ├── tabs.js
|
||||||
|
│ └── action_meta.js
|
||||||
|
├── actions/
|
||||||
|
│ ├── index.js # 统一导出所有动作
|
||||||
|
│ ├── amazon.js
|
||||||
|
│ └── amazon_tool.js
|
||||||
|
├── background/
|
||||||
|
│ └── index.js # 使用新的导入方式
|
||||||
|
└── examples/
|
||||||
|
└── usage_example.js # 使用示例
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 使用方式
|
||||||
|
|
||||||
|
### 1. 命名导入(推荐)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import {
|
||||||
|
ok_response,
|
||||||
|
fail_response,
|
||||||
|
create_tab_task,
|
||||||
|
getAllActionsMeta,
|
||||||
|
getActionByName
|
||||||
|
} from './libs/index.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 默认导入使用对象
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import Libs from './libs/index.js';
|
||||||
|
import Actions from './actions/index.js';
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
const response = Libs.response.ok({ data: 'success' });
|
||||||
|
const task = Libs.tabs.createTask('https://example.com');
|
||||||
|
const actions = Actions.amazon;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 混合使用
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 直接需要的函数用命名导入
|
||||||
|
import { ok_response, create_tab_task } from './libs/index.js';
|
||||||
|
|
||||||
|
// 复杂对象用默认导入
|
||||||
|
import Actions from './actions/index.js';
|
||||||
|
|
||||||
|
const action = getActionByName('amazon_search_list');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 统一导出内容
|
||||||
|
|
||||||
|
### libs/index.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 响应处理
|
||||||
|
export {
|
||||||
|
ok_response,
|
||||||
|
fail_response,
|
||||||
|
response_code,
|
||||||
|
guard_sync
|
||||||
|
} from './action_response.js';
|
||||||
|
|
||||||
|
// Tab 操作
|
||||||
|
export {
|
||||||
|
raw_execute_script,
|
||||||
|
inject_file,
|
||||||
|
ensure_injected,
|
||||||
|
execute_script,
|
||||||
|
open_tab,
|
||||||
|
close_tab,
|
||||||
|
create_tab_task
|
||||||
|
} from './tabs.js';
|
||||||
|
|
||||||
|
// 元数据处理
|
||||||
|
export {
|
||||||
|
bind_action_meta
|
||||||
|
} from './action_meta.js';
|
||||||
|
|
||||||
|
// 便捷对象
|
||||||
|
export const Libs = {
|
||||||
|
response: { ok: ok_response, fail: fail_response, ... },
|
||||||
|
tabs: { open: open_tab, close: close_tab, ... },
|
||||||
|
meta: { bindAction: bind_action_meta }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### actions/index.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 导出所有动作
|
||||||
|
export { amazon_actions } from './amazon.js';
|
||||||
|
|
||||||
|
// 导出工具函数
|
||||||
|
export {
|
||||||
|
injected_amazon_validate_captcha_continue,
|
||||||
|
run_amazon_pdp_action,
|
||||||
|
// ... 其他工具函数
|
||||||
|
} from './amazon_tool.js';
|
||||||
|
|
||||||
|
// 便捷函数
|
||||||
|
export function getAllActionsMeta() { ... }
|
||||||
|
export function getActionByName(name) { ... }
|
||||||
|
|
||||||
|
// 便捷对象
|
||||||
|
export const Actions = {
|
||||||
|
amazon: amazon_actions,
|
||||||
|
amazonTools: { ... }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 优势
|
||||||
|
|
||||||
|
1. **统一入口** - 不需要记住具体的文件路径
|
||||||
|
2. **灵活导入** - 支持命名导入、默认导入、混合导入
|
||||||
|
3. **便于维护** - 集中管理所有导出
|
||||||
|
4. **向后兼容** - 保持原有功能不变
|
||||||
|
5. **现代语法** - 使用最新的 ES6 模块特性
|
||||||
|
|
||||||
|
## 🔄 迁移指南
|
||||||
|
|
||||||
|
### 旧方式
|
||||||
|
```javascript
|
||||||
|
import { create_tab_task } from '../libs/tabs.js';
|
||||||
|
import { ok_response } from '../libs/action_response.js';
|
||||||
|
import { amazon_actions } from '../actions/amazon.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新方式
|
||||||
|
```javascript
|
||||||
|
import { create_tab_task, ok_response, amazon_actions } from '../libs/index.js';
|
||||||
|
// 或者
|
||||||
|
import { create_tab_task, ok_response } from '../libs/index.js';
|
||||||
|
import { amazon_actions } from '../actions/index.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 最佳实践
|
||||||
|
|
||||||
|
1. **简单函数** - 使用命名导入
|
||||||
|
2. **复杂对象** - 使用默认导入
|
||||||
|
3. **类型安全** - 配合 TypeScript 使用
|
||||||
|
4. **按需导入** - 只导入需要的功能
|
||||||
|
5. **统一风格** - 在一个项目中保持一致的导入风格
|
||||||
|
|
||||||
|
## 🔧 开发建议
|
||||||
|
|
||||||
|
- 新增功能时,优先添加到对应的 `index.js` 文件
|
||||||
|
- 保持导出名称的一致性和可读性
|
||||||
|
- 使用 JSDoc 注释提高代码可读性
|
||||||
|
- 定期检查和优化导出结构
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { create_tab_task } from '../libs/tabs.js';
|
import { create_tab_task, ok_response, fail_response, guard_sync, response_code } from '../libs/index.js';
|
||||||
import { fail_response, guard_sync, ok_response, response_code } from '../libs/action_response.js';
|
|
||||||
|
|
||||||
// Amazon:注入函数 + action 实现(amazon.js 仅保留 action 壳)
|
// Amazon:注入函数 + action 实现(amazon.js 仅保留 action 壳)
|
||||||
//
|
//
|
||||||
@@ -9,17 +8,15 @@ import { fail_response, guard_sync, ok_response, response_code } from '../libs/a
|
|||||||
|
|
||||||
// ---------- 页面注入(仅依赖页面 DOM) ----------
|
// ---------- 页面注入(仅依赖页面 DOM) ----------
|
||||||
|
|
||||||
function injected_utils() {
|
const injected_utils = () => window.__mv2_simple_injected || null;
|
||||||
return window.__mv2_simple_injected || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dispatch_human_click(target_el, options) {
|
const dispatch_human_click = (target_el, options) => {
|
||||||
const u = injected_utils();
|
const u = injected_utils();
|
||||||
if (u && typeof u.dispatch_human_click === 'function') {
|
if (u && typeof u.dispatch_human_click === 'function') {
|
||||||
return u.dispatch_human_click(target_el, options);
|
return u.dispatch_human_click(target_el, options);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function injected_amazon_validate_captcha_continue() {
|
export function injected_amazon_validate_captcha_continue() {
|
||||||
const href = location.href || '';
|
const href = location.href || '';
|
||||||
|
|||||||
63
mv2_simple_crx/src/actions/index.js
Normal file
63
mv2_simple_crx/src/actions/index.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 统一的动作导出
|
||||||
|
* 使用现代 ES6 模块化方式,提供统一的动作接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Amazon 相关动作
|
||||||
|
export { amazon_actions } from './amazon.js';
|
||||||
|
|
||||||
|
// Amazon 工具函数
|
||||||
|
export {
|
||||||
|
injected_amazon_validate_captcha_continue,
|
||||||
|
is_amazon_validate_captcha_url,
|
||||||
|
injected_amazon_product_detail,
|
||||||
|
injected_amazon_product_reviews,
|
||||||
|
run_amazon_pdp_action,
|
||||||
|
run_amazon_pdp_action_multi,
|
||||||
|
run_amazon_search_list_action,
|
||||||
|
run_amazon_set_language_action
|
||||||
|
} from './amazon_tool.js';
|
||||||
|
|
||||||
|
// 便捷的统一导出对象
|
||||||
|
export const Actions = {
|
||||||
|
// Amazon 动作列表
|
||||||
|
amazon: amazon_actions,
|
||||||
|
|
||||||
|
// Amazon 工具函数
|
||||||
|
amazonTools: {
|
||||||
|
validateCaptcha: injected_amazon_validate_captcha_continue,
|
||||||
|
isCaptchaUrl: is_amazon_validate_captcha_url,
|
||||||
|
productDetail: injected_amazon_product_detail,
|
||||||
|
productReviews: injected_amazon_product_reviews,
|
||||||
|
runPdpAction: run_amazon_pdp_action,
|
||||||
|
runPdpActionMulti: run_amazon_pdp_action_multi,
|
||||||
|
runSearchListAction: run_amazon_search_list_action,
|
||||||
|
runSetLanguageAction: run_amazon_set_language_action
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有动作的元信息
|
||||||
|
export function getAllActionsMeta() {
|
||||||
|
const meta = {};
|
||||||
|
if (Array.isArray(amazon_actions)) {
|
||||||
|
amazon_actions.forEach((item) => {
|
||||||
|
if (item && item.name) {
|
||||||
|
meta[item.name] = {
|
||||||
|
name: item.name,
|
||||||
|
desc: item.desc || '',
|
||||||
|
params: item.params || {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据名称获取动作
|
||||||
|
export function getActionByName(name) {
|
||||||
|
if (!Array.isArray(amazon_actions)) return null;
|
||||||
|
return amazon_actions.find(item => item && item.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认导出
|
||||||
|
export default Actions;
|
||||||
@@ -1,23 +1,32 @@
|
|||||||
|
|
||||||
import { amazon_actions } from '../actions/amazon.js';
|
import { amazon_actions, getAllActionsMeta, getActionByName } from '../actions/index.js';
|
||||||
|
|
||||||
|
// 调试日志系统
|
||||||
|
const DEBUG = true;
|
||||||
|
const debug_log = (level, ...args) => {
|
||||||
|
if (DEBUG) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
console[level](`[Background ${timestamp}]`, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// action 注册表:供 UI 下拉选择 + server bridge 调用
|
// action 注册表:供 UI 下拉选择 + server bridge 调用
|
||||||
const action_list = Array.isArray(amazon_actions) ? amazon_actions : [];
|
let action_list = [];
|
||||||
|
|
||||||
function list_actions_meta() {
|
try {
|
||||||
const meta = {};
|
if (Array.isArray(amazon_actions)) {
|
||||||
action_list.forEach((item) => {
|
action_list = amazon_actions.filter(item => item && typeof item === 'object' && item.name);
|
||||||
if (!item || !item.name) return;
|
debug_log('log', `Loaded ${action_list.length} actions:`, action_list.map(item => item.name));
|
||||||
meta[item.name] = {
|
} else {
|
||||||
name: item.name,
|
debug_log('warn', 'amazon_actions is not an array:', amazon_actions);
|
||||||
desc: item.desc || '',
|
}
|
||||||
params: item.params || {},
|
} catch (error) {
|
||||||
};
|
debug_log('error', 'Failed to load amazon_actions:', error);
|
||||||
});
|
|
||||||
return meta;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function create_action_send_response(sender) {
|
const list_actions_meta = () => getAllActionsMeta();
|
||||||
|
|
||||||
|
const 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 });
|
||||||
};
|
};
|
||||||
@@ -25,37 +34,48 @@ function create_action_send_response(sender) {
|
|||||||
emit_ui_event('push', { type: 'log', action: 'log', data: payload, sender });
|
emit_ui_event('push', { type: 'log', action: 'log', data: payload, sender });
|
||||||
};
|
};
|
||||||
return fn;
|
return fn;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ui_page_url = chrome.runtime.getURL('ui/index.html');
|
const ui_page_url = chrome.runtime.getURL('ui/index.html');
|
||||||
|
|
||||||
|
const emit_ui_event = (event_name, payload) => {
|
||||||
|
try {
|
||||||
function emit_ui_event(event_name, payload) {
|
chrome.runtime.sendMessage({
|
||||||
chrome.runtime.sendMessage({
|
channel: 'ui_event',
|
||||||
channel: 'ui_event',
|
event_name,
|
||||||
event_name,
|
payload,
|
||||||
payload,
|
ts: Date.now(),
|
||||||
ts: Date.now(),
|
}, (response) => {
|
||||||
});
|
if (chrome.runtime.lastError) {
|
||||||
}
|
console.warn('Failed to send UI event:', chrome.runtime.lastError.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in emit_ui_event:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
chrome.browserAction.onClicked.addListener(() => {
|
chrome.browserAction.onClicked.addListener(() => {
|
||||||
chrome.tabs.create({ url: ui_page_url, active: true });
|
chrome.tabs.create({ url: ui_page_url, active: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
debug_log('log', 'Received message:', { message, sender: sender.tab?.url || 'background' });
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
|
debug_log('warn', 'Empty message received');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI 自己发出来的事件,background 不处理
|
// UI 自己发出来的事件,background 不处理
|
||||||
if (message.channel === 'ui_event') {
|
if (message.channel === 'ui_event') {
|
||||||
|
debug_log('log', 'Ignoring ui_event message');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// content -> background 的推送消息(通用)
|
// content -> background 的推送消息(通用)
|
||||||
if (message.type === 'push') {
|
if (message.type === 'push') {
|
||||||
|
debug_log('log', 'Processing push message:', message.action);
|
||||||
emit_ui_event('push', {
|
emit_ui_event('push', {
|
||||||
type: 'push',
|
type: 'push',
|
||||||
action: message.action,
|
action: message.action,
|
||||||
@@ -67,18 +87,21 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
|
|
||||||
// UI -> background 的 action 调用
|
// UI -> background 的 action 调用
|
||||||
if (!message.action) {
|
if (!message.action) {
|
||||||
|
debug_log('error', 'Missing action in message');
|
||||||
sendResponse && sendResponse({ ok: false, error: '缺少 action' });
|
sendResponse && sendResponse({ ok: false, error: '缺少 action' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI 获取 action 元信息(用于下拉/默认参数)
|
// UI 获取 action 元信息(用于下拉/默认参数)
|
||||||
if (message.action === 'meta_actions') {
|
if (message.action === 'meta_actions') {
|
||||||
|
debug_log('log', 'Returning actions meta');
|
||||||
sendResponse({ ok: true, data: list_actions_meta() });
|
sendResponse({ ok: true, data: list_actions_meta() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI 刷新后台(重启 background page)
|
// UI 刷新后台(重启 background page)
|
||||||
if (message.action === 'reload_background') {
|
if (message.action === 'reload_background') {
|
||||||
|
debug_log('log', 'Reloading background page');
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
location.reload();
|
location.reload();
|
||||||
@@ -86,27 +109,47 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action_item = action_list.find((item) => item && item.name === message.action);
|
const action_item = getActionByName(message.action);
|
||||||
const action_handler = action_item && typeof action_item.handler === 'function' ? action_item.handler : null;
|
const action_handler = action_item && typeof action_item.handler === 'function' ? action_item.handler : null;
|
||||||
if (!action_handler) {
|
if (!action_handler) {
|
||||||
|
debug_log('error', 'Unknown action:', message.action);
|
||||||
sendResponse({ ok: false, error: '未知 action: ' + message.action });
|
sendResponse({ ok: false, error: '未知 action: ' + message.action });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request_id = `${Date.now()}_${Math.random().toString().slice(2)}`;
|
const request_id = `${Date.now()}_${Math.random().toString().slice(2)}`;
|
||||||
|
debug_log('log', 'Executing action:', { action: message.action, request_id, data: message.data });
|
||||||
emit_ui_event('request', { type: 'request', request_id, action: message.action, data: message.data || {}, sender });
|
emit_ui_event('request', { type: 'request', request_id, action: message.action, data: message.data || {}, sender });
|
||||||
|
|
||||||
const action_send_response = create_action_send_response(sender);
|
const action_send_response = create_action_send_response(sender);
|
||||||
|
|
||||||
|
// 添加超时处理
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
debug_log('warn', `Action ${message.action} timed out after 30000ms`);
|
||||||
|
emit_ui_event('response', {
|
||||||
|
type: 'response',
|
||||||
|
request_id,
|
||||||
|
ok: false,
|
||||||
|
error: 'Action timed out after 30 seconds',
|
||||||
|
sender
|
||||||
|
});
|
||||||
|
sendResponse({ ok: false, error: 'Action timed out after 30 seconds', request_id });
|
||||||
|
}, 30000); // 30秒超时
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await action_handler(message.data || {}, action_send_response);
|
const res = await action_handler(message.data || {}, action_send_response);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
debug_log('log', `Action ${message.action} completed successfully:`, { request_id, result: res });
|
||||||
emit_ui_event('response', { type: 'response', request_id, ok: true, data: res, sender });
|
emit_ui_event('response', { type: 'response', request_id, ok: true, data: res, sender });
|
||||||
sendResponse({ ok: true, data: res, request_id });
|
sendResponse({ ok: true, data: res, request_id });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
clearTimeout(timeout);
|
||||||
const error = (err && err.message) || String(err);
|
const error = (err && err.message) || String(err);
|
||||||
emit_ui_event('response', { type: 'response', request_id, ok: false, error, sender });
|
const stack = (err && err.stack) || '';
|
||||||
sendResponse({ ok: false, error, request_id });
|
debug_log('error', `Action ${message.action} failed:`, { error, stack, data: message.data });
|
||||||
|
emit_ui_event('response', { type: 'response', request_id, ok: false, error, stack, sender });
|
||||||
|
sendResponse({ ok: false, error, stack, request_id });
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
60
mv2_simple_crx/src/examples/usage_example.js
Normal file
60
mv2_simple_crx/src/examples/usage_example.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 使用新模块化方式的示例
|
||||||
|
* 展示如何使用统一的导出接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 方式1: 命名导入(推荐)
|
||||||
|
import {
|
||||||
|
ok_response,
|
||||||
|
fail_response,
|
||||||
|
create_tab_task,
|
||||||
|
amazon_actions,
|
||||||
|
getAllActionsMeta,
|
||||||
|
getActionByName
|
||||||
|
} from '../libs/index.js';
|
||||||
|
|
||||||
|
// 方式2: 默认导入使用对象
|
||||||
|
import Libs from '../libs/index.js';
|
||||||
|
import Actions from '../actions/index.js';
|
||||||
|
|
||||||
|
// 示例函数
|
||||||
|
export async function exampleAction() {
|
||||||
|
// 使用命名导入
|
||||||
|
const response = ok_response({ success: true });
|
||||||
|
|
||||||
|
// 使用默认导入
|
||||||
|
const task = Libs.tabs.createTask('https://example.com');
|
||||||
|
|
||||||
|
// 使用 Actions
|
||||||
|
const allMeta = getAllActionsMeta();
|
||||||
|
const specificAction = getActionByName('amazon_search_list');
|
||||||
|
|
||||||
|
return {
|
||||||
|
response,
|
||||||
|
task,
|
||||||
|
allMeta,
|
||||||
|
specificAction
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更简洁的写法
|
||||||
|
export const ModernUsage = {
|
||||||
|
// 响应处理
|
||||||
|
response: {
|
||||||
|
success: (data) => ok_response(data),
|
||||||
|
error: (msg) => fail_response(msg)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tab 操作
|
||||||
|
tabs: {
|
||||||
|
create: (url) => create_tab_task(url),
|
||||||
|
// ... 其他操作
|
||||||
|
},
|
||||||
|
|
||||||
|
// 动作管理
|
||||||
|
actions: {
|
||||||
|
getAll: getAllActionsMeta,
|
||||||
|
get: getActionByName,
|
||||||
|
list: amazon_actions
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,60 +1,58 @@
|
|||||||
(function () {
|
(() => {
|
||||||
if (window.__mv2_simple_injected) return;
|
if (window.__mv2_simple_injected) return;
|
||||||
|
|
||||||
function norm_space(s) {
|
const norm_space = (s) => (s || '').toString().replace(/\s+/g, ' ').trim();
|
||||||
return (s || '').toString().replace(/\s+/g, ' ').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function busy_wait_ms(ms) {
|
const busy_wait_ms = (ms) => {
|
||||||
var t = Number(ms);
|
const t = Number(ms);
|
||||||
var dur = Number.isFinite(t) ? Math.max(0, t) : 0;
|
const dur = Number.isFinite(t) ? Math.max(0, t) : 0;
|
||||||
var t0 = performance.now();
|
const t0 = performance.now();
|
||||||
while (performance.now() - t0 < dur) { }
|
while (performance.now() - t0 < dur) { }
|
||||||
}
|
};
|
||||||
|
|
||||||
function is_visible(el) {
|
const is_visible = (el) => {
|
||||||
if (!el) return false;
|
if (!el) return false;
|
||||||
var r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
if (!(r.width > 0 && r.height > 0)) return false;
|
if (!(r.width > 0 && r.height > 0)) return false;
|
||||||
// 尽量避免点击到不可见层;display/visibility 由浏览器计算
|
// 尽量避免点击到不可见层;display/visibility 由浏览器计算
|
||||||
var cs = window.getComputedStyle(el);
|
const cs = window.getComputedStyle(el);
|
||||||
if (!cs) return true;
|
if (!cs) return true;
|
||||||
if (cs.display === 'none') return false;
|
if (cs.display === 'none') return false;
|
||||||
if (cs.visibility === 'hidden') return false;
|
if (cs.visibility === 'hidden') return false;
|
||||||
if (cs.opacity === '0') return false;
|
if (cs.opacity === '0') return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
function wait_query(selectors, timeout_ms) {
|
const wait_query = (selectors, timeout_ms) => {
|
||||||
var list = Array.isArray(selectors) ? selectors : [];
|
const list = Array.isArray(selectors) ? selectors : [];
|
||||||
var deadline = Date.now() + (Number.isFinite(timeout_ms) ? timeout_ms : 5000);
|
const deadline = Date.now() + (Number.isFinite(timeout_ms) ? timeout_ms : 5000);
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
for (var i = 0; i < list.length; i += 1) {
|
for (let i = 0; i < list.length; i += 1) {
|
||||||
var sel = list[i];
|
const sel = list[i];
|
||||||
var el = document.querySelector(sel);
|
const el = document.querySelector(sel);
|
||||||
if (is_visible(el)) return el;
|
if (is_visible(el)) return el;
|
||||||
}
|
}
|
||||||
busy_wait_ms(40);
|
busy_wait_ms(40);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|
||||||
function dispatch_human_click(target_el, options) {
|
const dispatch_human_click = (target_el, options) => {
|
||||||
var el = target_el;
|
const el = target_el;
|
||||||
if (!el) return false;
|
if (!el) return false;
|
||||||
var opt = options && typeof options === 'object' ? options : {};
|
const opt = options && typeof options === 'object' ? options : {};
|
||||||
var pointer_id = Number.isFinite(opt.pointer_id) ? opt.pointer_id : 1;
|
const pointer_id = Number.isFinite(opt.pointer_id) ? opt.pointer_id : 1;
|
||||||
var pointer_type = opt.pointer_type ? String(opt.pointer_type) : 'mouse';
|
const pointer_type = opt.pointer_type ? String(opt.pointer_type) : 'mouse';
|
||||||
|
|
||||||
try { el.scrollIntoView({ block: 'center', inline: 'center' }); } catch (_) { }
|
try { el.scrollIntoView({ block: 'center', inline: 'center' }); } catch (_) { }
|
||||||
try { el.focus && el.focus(); } catch (_) { }
|
try { el.focus && el.focus(); } catch (_) { }
|
||||||
|
|
||||||
var rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
var ox = Number.isFinite(opt.offset_x) ? opt.offset_x : 0;
|
const ox = Number.isFinite(opt.offset_x) ? opt.offset_x : 0;
|
||||||
var oy = Number.isFinite(opt.offset_y) ? opt.offset_y : 0;
|
const oy = Number.isFinite(opt.offset_y) ? opt.offset_y : 0;
|
||||||
var x = Math.max(1, Math.floor(rect.left + rect.width / 2 + ox));
|
const x = Math.max(1, Math.floor(rect.left + rect.width / 2 + ox));
|
||||||
var y = Math.max(1, Math.floor(rect.top + rect.height / 2 + oy));
|
const y = Math.max(1, Math.floor(rect.top + rect.height / 2 + oy));
|
||||||
var base = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
|
const base = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof PointerEvent === 'function') {
|
if (typeof PointerEvent === 'function') {
|
||||||
@@ -72,58 +70,54 @@
|
|||||||
el.dispatchEvent(new MouseEvent('mouseup', base));
|
el.dispatchEvent(new MouseEvent('mouseup', base));
|
||||||
el.dispatchEvent(new MouseEvent('click', base));
|
el.dispatchEvent(new MouseEvent('click', base));
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
function text(el) {
|
const text = (el) => el && el.textContent != null ? norm_space(el.textContent) : null;
|
||||||
return el && el.textContent != null ? norm_space(el.textContent) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inner_text(el) {
|
const inner_text = (el) => el && el.innerText != null ? norm_space(el.innerText) : null;
|
||||||
return el && el.innerText != null ? norm_space(el.innerText) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function attr(el, name) {
|
const attr = (el, name) => {
|
||||||
if (!el || !name) return null;
|
if (!el || !name) return null;
|
||||||
var v = el.getAttribute ? el.getAttribute(name) : null;
|
const v = el.getAttribute ? el.getAttribute(name) : null;
|
||||||
return v != null ? norm_space(v) : null;
|
return v != null ? norm_space(v) : null;
|
||||||
}
|
};
|
||||||
|
|
||||||
function abs_url(href, base) {
|
const abs_url = (href, base) => {
|
||||||
try {
|
try {
|
||||||
return new URL(href, base || location.origin).toString();
|
return new URL(href, base || location.origin).toString();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return href;
|
return href;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function parse_asin_from_url(url) {
|
const parse_asin_from_url = (url) => {
|
||||||
if (!url || typeof url !== 'string') return null;
|
if (!url || typeof url !== 'string') return null;
|
||||||
var m = url.match(/\/dp\/([A-Z0-9]{10})/i) || url.match(/\/gp\/product\/([A-Z0-9]{10})/i);
|
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;
|
return m ? m[1].toUpperCase() : null;
|
||||||
}
|
};
|
||||||
|
|
||||||
function pick_number(text0) {
|
const pick_number = (text0) => {
|
||||||
if (!text0) return null;
|
if (!text0) return null;
|
||||||
var s = String(text0).replace(/[(),]/g, ' ').replace(/\s+/g, ' ').trim();
|
const s = String(text0).replace(/[(),]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
var m = s.match(/(\d+(?:\.\d+)?)/);
|
const m = s.match(/(\d+(?:\.\d+)?)/);
|
||||||
return m ? Number(m[1]) : null;
|
return m ? Number(m[1]) : null;
|
||||||
}
|
};
|
||||||
|
|
||||||
function pick_int(text0) {
|
const pick_int = (text0) => {
|
||||||
if (!text0) return null;
|
if (!text0) return null;
|
||||||
var raw = String(text0).replace(/\s+/g, ' ').trim();
|
const raw = String(text0).replace(/\s+/g, ' ').trim();
|
||||||
var up = raw.toUpperCase().replace(/,/g, '');
|
const up = raw.toUpperCase().replace(/,/g, '');
|
||||||
var km = up.match(/([\d.]+)\s*K\b/);
|
const km = up.match(/([\d.]+)\s*K\b/);
|
||||||
if (km) return Math.round(parseFloat(km[1]) * 1000);
|
if (km) return Math.round(parseFloat(km[1]) * 1000);
|
||||||
var mm = up.match(/([\d.]+)\s*M\b/);
|
const mm = up.match(/([\d.]+)\s*M\b/);
|
||||||
if (mm) return Math.round(parseFloat(mm[1]) * 1000000);
|
if (mm) return Math.round(parseFloat(mm[1]) * 1000000);
|
||||||
var digits = raw.replace(/[^\d]/g, '');
|
const digits = raw.replace(/[^\d]/g, '');
|
||||||
return digits ? Number(digits) : null;
|
return digits ? Number(digits) : null;
|
||||||
}
|
};
|
||||||
|
|
||||||
function set_input_value(input, value, options) {
|
const set_input_value = (input, value, options) => {
|
||||||
if (!input) return false;
|
if (!input) return false;
|
||||||
var opt = options && typeof options === 'object' ? options : {};
|
const opt = options && typeof options === 'object' ? options : {};
|
||||||
try { input.focus && input.focus(); } catch (_) { }
|
try { input.focus && input.focus(); } catch (_) { }
|
||||||
try { input.value = value == null ? '' : String(value); } catch (_) { return false; }
|
try { input.value = value == null ? '' : String(value); } catch (_) { return false; }
|
||||||
if (opt.dispatch_input !== false) {
|
if (opt.dispatch_input !== false) {
|
||||||
@@ -133,7 +127,7 @@
|
|||||||
try { input.dispatchEvent(new Event('change', { bubbles: true })); } catch (_) { }
|
try { input.dispatchEvent(new Event('change', { bubbles: true })); } catch (_) { }
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
window.__mv2_simple_injected = {
|
window.__mv2_simple_injected = {
|
||||||
norm_space: norm_space,
|
norm_space: norm_space,
|
||||||
|
|||||||
58
mv2_simple_crx/src/libs/index.js
Normal file
58
mv2_simple_crx/src/libs/index.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* 统一的库函数导出
|
||||||
|
* 使用现代 ES6 模块化方式,提供统一的功能接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 响应处理相关
|
||||||
|
export {
|
||||||
|
ok_response,
|
||||||
|
fail_response,
|
||||||
|
response_code,
|
||||||
|
guard_sync
|
||||||
|
} from './action_response.js';
|
||||||
|
|
||||||
|
// Tab 操作相关
|
||||||
|
export {
|
||||||
|
raw_execute_script,
|
||||||
|
inject_file,
|
||||||
|
ensure_injected,
|
||||||
|
execute_script,
|
||||||
|
open_tab,
|
||||||
|
close_tab,
|
||||||
|
create_tab_task
|
||||||
|
} from './tabs.js';
|
||||||
|
|
||||||
|
// Action 元数据相关
|
||||||
|
export {
|
||||||
|
bind_action_meta
|
||||||
|
} from './action_meta.js';
|
||||||
|
|
||||||
|
// 便捷的统一导出对象(可选使用)
|
||||||
|
export const Libs = {
|
||||||
|
// 响应处理
|
||||||
|
response: {
|
||||||
|
ok: ok_response,
|
||||||
|
fail: fail_response,
|
||||||
|
code: response_code,
|
||||||
|
guard: guard_sync
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tab 操作
|
||||||
|
tabs: {
|
||||||
|
rawExecuteScript: raw_execute_script,
|
||||||
|
injectFile: inject_file,
|
||||||
|
ensureInjected: ensure_injected,
|
||||||
|
executeScript: execute_script,
|
||||||
|
open: open_tab,
|
||||||
|
close: close_tab,
|
||||||
|
createTask: create_tab_task
|
||||||
|
},
|
||||||
|
|
||||||
|
// 元数据
|
||||||
|
meta: {
|
||||||
|
bindAction: bind_action_meta
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 默认导出(可选使用)
|
||||||
|
export default Libs;
|
||||||
@@ -1,85 +1,277 @@
|
|||||||
// openTab:MV2 版本(极简 + 回调风格)
|
// openTab:MV2 版本(极简 + 回调风格)
|
||||||
|
|
||||||
function build_code(fn, args) {
|
/**
|
||||||
|
* 构建可执行代码字符串
|
||||||
|
* @param {Function|string} fn - 要执行的函数或代码字符串
|
||||||
|
* @param {Array} args - 传递给函数的参数数组
|
||||||
|
* @returns {string} 可执行的代码字符串
|
||||||
|
*/
|
||||||
|
const build_code = (fn, args) => {
|
||||||
if (typeof fn === 'function') {
|
if (typeof fn === 'function') {
|
||||||
if (Array.isArray(args) && args.length) {
|
const funcStr = fn.toString();
|
||||||
return `(${fn.toString()}).apply(null, ${JSON.stringify(args)});`;
|
if (Array.isArray(args) && args.length > 0) {
|
||||||
|
// 安全地序列化参数,避免循环引用
|
||||||
|
const serializedArgs = JSON.stringify(args, (key, value) => {
|
||||||
|
if (typeof value === 'function') return undefined;
|
||||||
|
if (value && typeof value === 'object' && value.constructor === Object) {
|
||||||
|
try {
|
||||||
|
JSON.stringify(value);
|
||||||
|
return value;
|
||||||
|
} catch {
|
||||||
|
return '[Object]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
return `(${funcStr}).apply(null, ${serializedArgs});`;
|
||||||
}
|
}
|
||||||
return `(${fn.toString()})();`;
|
return `(${funcStr})();`;
|
||||||
}
|
}
|
||||||
return fn;
|
|
||||||
}
|
if (typeof fn === 'string') {
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError('fn must be a function or string');
|
||||||
|
};
|
||||||
|
|
||||||
// 低阶:只负责执行,不做任何前置注入。
|
/**
|
||||||
export function raw_execute_script(tab_id, fn, args, run_at) {
|
* 在指定标签页中执行原始脚本(低阶接口,不做任何前置注入)
|
||||||
const real_run_at = run_at || 'document_idle';
|
* @param {number} tab_id - 标签页ID
|
||||||
const code = build_code(fn, args);
|
* @param {Function|string} fn - 要执行的函数或代码字符串
|
||||||
|
* @param {Array} args - 传递给函数的参数数组
|
||||||
|
* @param {string} run_at - 执行时机:'document_start' | 'document_end' | 'document_idle'
|
||||||
|
* @returns {Promise<Array>} 执行结果数组
|
||||||
|
*/
|
||||||
|
export async function raw_execute_script(tab_id, fn, args = [], run_at = 'document_idle') {
|
||||||
|
// 参数验证
|
||||||
|
if (!Number.isInteger(tab_id) || tab_id <= 0) {
|
||||||
|
throw new Error('Invalid tab_id: must be a positive integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fn || (typeof fn !== 'function' && typeof fn !== 'string')) {
|
||||||
|
throw new Error('Invalid fn: must be a function or string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(args)) {
|
||||||
|
throw new Error('Invalid args: must be an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
const validRunAt = ['document_start', 'document_end', 'document_idle'];
|
||||||
|
if (!validRunAt.includes(run_at)) {
|
||||||
|
throw new Error(`Invalid run_at: must be one of ${validRunAt.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
chrome.tabs.executeScript(
|
const code = build_code(fn, args);
|
||||||
tab_id,
|
|
||||||
{
|
return await new Promise((resolve, reject) => {
|
||||||
code,
|
const timeoutId = setTimeout(() => {
|
||||||
runAt: real_run_at,
|
reject(new Error(`Script execution timeout for tab ${tab_id}`));
|
||||||
},
|
}, 30000); // 30秒超时
|
||||||
(result) => {
|
|
||||||
if (chrome.runtime.lastError) {
|
chrome.tabs.executeScript(
|
||||||
return reject(new Error(chrome.runtime.lastError.message));
|
tab_id,
|
||||||
|
{
|
||||||
|
code,
|
||||||
|
runAt: run_at,
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
const error = new Error(chrome.runtime.lastError.message);
|
||||||
|
error.code = chrome.runtime.lastError.message?.includes('No tab with id') ? 'TAB_NOT_FOUND' : 'EXECUTION_ERROR';
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result || []);
|
||||||
}
|
}
|
||||||
resolve(result);
|
);
|
||||||
},
|
});
|
||||||
);
|
} catch (error) {
|
||||||
});
|
// 重新抛出带有更多上下文的错误
|
||||||
|
const enhancedError = new Error(`Failed to execute script in tab ${tab_id}: ${error.message}`);
|
||||||
|
enhancedError.originalError = error;
|
||||||
|
enhancedError.tabId = tab_id;
|
||||||
|
enhancedError.runAt = run_at;
|
||||||
|
throw enhancedError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inject_file(tab_id, file, run_at) {
|
/**
|
||||||
const real_run_at = run_at || 'document_idle';
|
* 在指定标签页中注入文件
|
||||||
return new Promise((resolve, reject) => {
|
* @param {number} tab_id - 标签页ID
|
||||||
chrome.tabs.executeScript(
|
* @param {string} file - 要注入的文件路径(相对于扩展根目录)
|
||||||
tab_id,
|
* @param {string} run_at - 执行时机:'document_start' | 'document_end' | 'document_idle'
|
||||||
{
|
* @returns {Promise<boolean>} 注入是否成功
|
||||||
file,
|
*/
|
||||||
runAt: real_run_at,
|
export async function inject_file(tab_id, file, run_at = 'document_idle') {
|
||||||
},
|
// 参数验证
|
||||||
() => {
|
if (!Number.isInteger(tab_id) || tab_id <= 0) {
|
||||||
if (chrome.runtime.lastError) {
|
throw new Error('Invalid tab_id: must be a positive integer');
|
||||||
return reject(new Error(chrome.runtime.lastError.message));
|
}
|
||||||
|
|
||||||
|
if (!file || typeof file !== 'string') {
|
||||||
|
throw new Error('Invalid file: must be a non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件路径格式
|
||||||
|
if (!file.match(/^[\w\-./]+$/)) {
|
||||||
|
throw new Error('Invalid file path: contains invalid characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
const validRunAt = ['document_start', 'document_end', 'document_idle'];
|
||||||
|
if (!validRunAt.includes(run_at)) {
|
||||||
|
throw new Error(`Invalid run_at: must be one of ${validRunAt.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
reject(new Error(`File injection timeout for tab ${tab_id}: ${file}`));
|
||||||
|
}, 15000); // 15秒超时
|
||||||
|
|
||||||
|
chrome.tabs.executeScript(
|
||||||
|
tab_id,
|
||||||
|
{
|
||||||
|
file,
|
||||||
|
runAt: run_at,
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
const error = new Error(chrome.runtime.lastError.message);
|
||||||
|
error.code = chrome.runtime.lastError.message?.includes('No tab with id') ? 'TAB_NOT_FOUND' :
|
||||||
|
chrome.runtime.lastError.message?.includes('Cannot access') ? 'FILE_NOT_FOUND' : 'INJECTION_ERROR';
|
||||||
|
error.file = file;
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
}
|
}
|
||||||
resolve(true);
|
);
|
||||||
},
|
});
|
||||||
);
|
} catch (error) {
|
||||||
});
|
// 重新抛出带有更多上下文的错误
|
||||||
|
const enhancedError = new Error(`Failed to inject file "${file}" in tab ${tab_id}: ${error.message}`);
|
||||||
|
enhancedError.originalError = error;
|
||||||
|
enhancedError.tabId = tab_id;
|
||||||
|
enhancedError.file = file;
|
||||||
|
enhancedError.runAt = run_at;
|
||||||
|
throw enhancedError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pick_first_frame_value(raw_list) {
|
/**
|
||||||
|
* 从执行结果中提取第一个帧的值
|
||||||
|
* @param {Array} raw_list - Chrome 执行脚本返回的结果数组
|
||||||
|
* @returns {*} 第一个帧的值,如果数组为空则返回 null
|
||||||
|
*/
|
||||||
|
const pick_first_frame_value = (raw_list) => {
|
||||||
if (!Array.isArray(raw_list) || raw_list.length === 0) return null;
|
if (!Array.isArray(raw_list) || raw_list.length === 0) return null;
|
||||||
return raw_list[0];
|
return raw_list[0]?.result ?? raw_list[0];
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function ensure_injected(tab_id) {
|
/**
|
||||||
const injected = pick_first_frame_value(
|
* 确保注入脚本已加载到指定标签页
|
||||||
await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle'),
|
* @param {number} tab_id - 标签页ID
|
||||||
);
|
* @param {number} maxRetries - 最大重试次数(默认3次)
|
||||||
if (injected === true) return true;
|
* @returns {Promise<boolean>} 注入是否成功
|
||||||
|
*/
|
||||||
// 约定:扩展根目录=src,因此 file 使用 src 内相对路径
|
export async function ensure_injected(tab_id, maxRetries = 3) {
|
||||||
await inject_file(tab_id, 'injected/injected.js', 'document_idle');
|
if (!Number.isInteger(tab_id) || tab_id <= 0) {
|
||||||
const injected2 = pick_first_frame_value(
|
throw new Error('Invalid tab_id: must be a positive integer');
|
||||||
await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle'),
|
|
||||||
);
|
|
||||||
return injected2 === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 高阶:默认确保 injected 通用方法已加载。
|
|
||||||
export async function execute_script(tab_id, fn, args, run_at, options) {
|
|
||||||
const ensure = !(options && options.ensure_injected === false);
|
|
||||||
if (ensure) {
|
|
||||||
await ensure_injected(tab_id);
|
|
||||||
}
|
}
|
||||||
return await raw_execute_script(tab_id, fn, args, run_at);
|
|
||||||
|
// 检查是否已经注入
|
||||||
|
try {
|
||||||
|
const injected = pick_first_frame_value(
|
||||||
|
await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle')
|
||||||
|
);
|
||||||
|
if (injected === true) return true;
|
||||||
|
} catch (error) {
|
||||||
|
// 如果检查失败,可能是标签页不存在,继续尝试注入
|
||||||
|
console.warn(`Failed to check injection status for tab ${tab_id}:`, error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试注入,带重试机制
|
||||||
|
let lastError;
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
// 约定:扩展根目录=src,因此 file 使用 src 内相对路径
|
||||||
|
await inject_file(tab_id, 'injected/injected.js', 'document_idle');
|
||||||
|
|
||||||
|
// 验证注入是否成功
|
||||||
|
const injected = pick_first_frame_value(
|
||||||
|
await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (injected === true) return true;
|
||||||
|
|
||||||
|
// 如果注入后仍然失败,等待一小段时间再重试
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500 * attempt));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Failed to ensure injection after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_tab(tab_id, update_props) {
|
/**
|
||||||
|
* 高阶脚本执行接口(默认确保 injected 通用方法已加载)
|
||||||
|
* @param {number} tab_id - 标签页ID
|
||||||
|
* @param {Function|string} fn - 要执行的函数或代码字符串
|
||||||
|
* @param {Array} args - 传递给函数的参数数组
|
||||||
|
* @param {string} run_at - 执行时机
|
||||||
|
* @param {Object} options - 选项配置
|
||||||
|
* @param {boolean} options.ensure_injected - 是否确保注入(默认true)
|
||||||
|
* @param {number} options.maxRetries - 注入重试次数(默认3次)
|
||||||
|
* @returns {Promise<Array>} 执行结果数组
|
||||||
|
*/
|
||||||
|
export async function execute_script(tab_id, fn, args = [], run_at = 'document_idle', options = {}) {
|
||||||
|
// 参数验证
|
||||||
|
if (!Number.isInteger(tab_id) || tab_id <= 0) {
|
||||||
|
throw new Error('Invalid tab_id: must be a positive integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fn || (typeof fn !== 'function' && typeof fn !== 'string')) {
|
||||||
|
throw new Error('Invalid fn: must be a function or string');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选项配置
|
||||||
|
const opts = {
|
||||||
|
ensure_injected: true,
|
||||||
|
maxRetries: 3,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 确保注入(如果需要)
|
||||||
|
if (opts.ensure_injected) {
|
||||||
|
await ensure_injected(tab_id, opts.maxRetries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行脚本
|
||||||
|
return await raw_execute_script(tab_id, fn, args, run_at);
|
||||||
|
} catch (error) {
|
||||||
|
// 增强错误信息
|
||||||
|
const enhancedError = new Error(`Failed to execute script in tab ${tab_id}: ${error.message}`);
|
||||||
|
enhancedError.originalError = error;
|
||||||
|
enhancedError.tabId = tab_id;
|
||||||
|
enhancedError.ensureInjected = opts.ensure_injected;
|
||||||
|
throw enhancedError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const update_tab = (tab_id, update_props) => {
|
||||||
return new Promise((resolve_update, reject_update) => {
|
return new Promise((resolve_update, reject_update) => {
|
||||||
chrome.tabs.update(tab_id, update_props, (updated_tab) => {
|
chrome.tabs.update(tab_id, update_props, (updated_tab) => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
@@ -88,9 +280,9 @@ function update_tab(tab_id, update_props) {
|
|||||||
resolve_update(updated_tab || true);
|
resolve_update(updated_tab || true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
function attach_tab_helpers(tab) {
|
const attach_tab_helpers = (tab) => {
|
||||||
if (!tab) return tab;
|
if (!tab) return tab;
|
||||||
|
|
||||||
tab.remove = function remove(delay_ms) {
|
tab.remove = function remove(delay_ms) {
|
||||||
@@ -227,50 +419,178 @@ function attach_tab_helpers(tab) {
|
|||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function open_tab(url, options) {
|
/**
|
||||||
// 保留原本 Promise 版本(内部复用)
|
* 打开新标签页并等待加载完成
|
||||||
options = options && typeof options === 'object' ? options : {};
|
* @param {string} url - 要打开的URL
|
||||||
|
* @param {Object} options - 选项配置
|
||||||
|
* @param {boolean} options.active - 是否激活标签页(默认true)
|
||||||
|
* @param {number} options.timeout - 加载超时时间(毫秒,默认45000)
|
||||||
|
* @param {boolean} options.loadInBackground - 是否在后台加载(默认false)
|
||||||
|
* @returns {Promise<{tab_id: number, tab: Object}>} 标签页信息
|
||||||
|
*/
|
||||||
|
export async function open_tab(url, options = {}) {
|
||||||
|
// 参数验证
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
throw new Error('Invalid url: must be a non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
// 验证URL格式
|
||||||
chrome.tabs.create(
|
try {
|
||||||
{
|
new URL(url);
|
||||||
url: 'about:blank',
|
} catch {
|
||||||
active: options.active !== false,
|
throw new Error('Invalid url format: must be a valid URL');
|
||||||
},
|
}
|
||||||
(tab) => {
|
|
||||||
if (chrome.runtime.lastError) {
|
// 选项配置
|
||||||
return reject(new Error(chrome.runtime.lastError.message));
|
const opts = {
|
||||||
}
|
active: true,
|
||||||
if (!tab || !tab.id) {
|
timeout: 45000, // 45秒超时
|
||||||
return reject(new Error('tab 创建失败'));
|
loadInBackground: false,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof opts.active !== 'boolean') {
|
||||||
|
throw new Error('Invalid options.active: must be a boolean');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(opts.timeout) || opts.timeout <= 0) {
|
||||||
|
throw new Error('Invalid options.timeout: must be a positive integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
// 设置超时
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
chrome.tabs.onUpdated.removeListener(on_updated);
|
||||||
|
reject(new Error(`Tab loading timeout for ${url} after ${opts.timeout}ms`));
|
||||||
|
}, opts.timeout);
|
||||||
|
|
||||||
|
const on_updated = (updated_tab_id, change_info, updated_tab) => {
|
||||||
|
if (updated_tab_id !== tab_id) return;
|
||||||
|
if (change_info.status !== 'complete') return;
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
chrome.tabs.onUpdated.removeListener(on_updated);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const enhancedTab = attach_tab_helpers(updated_tab);
|
||||||
|
resolve({ tab_id, tab: enhancedTab });
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error(`Failed to attach helpers to tab ${tab_id}: ${error.message}`));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const tab_id = tab.id;
|
chrome.tabs.onUpdated.addListener(on_updated);
|
||||||
|
|
||||||
const on_updated = (updated_tab_id, change_info, updated_tab) => {
|
// 创建标签页
|
||||||
if (updated_tab_id !== tab_id) return;
|
chrome.tabs.create(
|
||||||
if (change_info.status !== 'complete') return;
|
{
|
||||||
|
url: 'about:blank',
|
||||||
chrome.tabs.onUpdated.removeListener(on_updated);
|
active: !opts.loadInBackground && opts.active,
|
||||||
resolve({ tab_id, tab: attach_tab_helpers(updated_tab) });
|
},
|
||||||
};
|
async (tab) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
chrome.tabs.onUpdated.addListener(on_updated);
|
clearTimeout(timeoutId);
|
||||||
update_tab(tab_id, { url })
|
|
||||||
.catch((err) => {
|
|
||||||
chrome.tabs.onUpdated.removeListener(on_updated);
|
chrome.tabs.onUpdated.removeListener(on_updated);
|
||||||
reject(err);
|
const error = new Error(chrome.runtime.lastError.message);
|
||||||
});
|
error.code = 'TAB_CREATE_FAILED';
|
||||||
},
|
return reject(error);
|
||||||
);
|
}
|
||||||
});
|
|
||||||
|
if (!tab || !tab.id) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
chrome.tabs.onUpdated.removeListener(on_updated);
|
||||||
|
return reject(new Error('Failed to create tab: invalid tab object'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab_id = tab.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 导航到目标URL
|
||||||
|
await update_tab(tab_id, { url });
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
chrome.tabs.onUpdated.removeListener(on_updated);
|
||||||
|
reject(new Error(`Failed to navigate tab ${tab_id} to ${url}: ${error.message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 增强错误信息
|
||||||
|
const enhancedError = new Error(`Failed to open tab for ${url}: ${error.message}`);
|
||||||
|
enhancedError.originalError = error;
|
||||||
|
enhancedError.url = url;
|
||||||
|
throw enhancedError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function close_tab(tab_id, delay_ms) {
|
/**
|
||||||
delay_ms = Number.isFinite(delay_ms) ? delay_ms : 0;
|
* 关闭指定标签页
|
||||||
setTimeout(() => {
|
* @param {number} tab_id - 标签页ID
|
||||||
chrome.tabs.remove(tab_id, () => void 0);
|
* @param {number|Object} delayOrOptions - 延迟时间(毫秒)或选项对象
|
||||||
}, Math.max(0, delay_ms));
|
* @param {number} delayOrOptions.delay - 延迟时间(毫秒,默认0)
|
||||||
|
* @param {boolean} delayOrOptions.force - 是否强制关闭(默认false)
|
||||||
|
* @returns {Promise<boolean>} 关闭是否成功
|
||||||
|
*/
|
||||||
|
export async function close_tab(tab_id, delayOrOptions = {}) {
|
||||||
|
// 参数验证
|
||||||
|
if (!Number.isInteger(tab_id) || tab_id <= 0) {
|
||||||
|
throw new Error('Invalid tab_id: must be a positive integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理选项参数
|
||||||
|
let options = {};
|
||||||
|
if (typeof delayOrOptions === 'number') {
|
||||||
|
options.delay = delayOrOptions;
|
||||||
|
} else if (typeof delayOrOptions === 'object') {
|
||||||
|
options = delayOrOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
delay: 0,
|
||||||
|
force: false,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!Number.isInteger(opts.delay) || opts.delay < 0) {
|
||||||
|
throw new Error('Invalid delay: must be a non-negative integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const delay = Math.max(0, opts.delay);
|
||||||
|
|
||||||
|
if (delay === 0) {
|
||||||
|
// 立即关闭
|
||||||
|
chrome.tabs.remove(tab_id, () => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
const error = new Error(chrome.runtime.lastError.message);
|
||||||
|
error.code = chrome.runtime.lastError.message?.includes('No tab with id') ? 'TAB_NOT_FOUND' : 'CLOSE_FAILED';
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 延迟关闭
|
||||||
|
setTimeout(() => {
|
||||||
|
chrome.tabs.remove(tab_id, () => {
|
||||||
|
if (chrome.runtime.lastError && !opts.force) {
|
||||||
|
const error = new Error(chrome.runtime.lastError.message);
|
||||||
|
error.code = chrome.runtime.lastError.message?.includes('No tab with id') ? 'TAB_NOT_FOUND' : 'CLOSE_FAILED';
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const enhancedError = new Error(`Failed to close tab ${tab_id}: ${error.message}`);
|
||||||
|
enhancedError.originalError = error;
|
||||||
|
enhancedError.tabId = tab_id;
|
||||||
|
throw enhancedError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// openTab 任务对象:用对象绑定方法,减少重复参数
|
// openTab 任务对象:用对象绑定方法,减少重复参数
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"description": "MV2 极简骨架:openTab + executeScript + __REQUEST_DONE 监听",
|
"description": "MV2 极简骨架:openTab + executeScript + __REQUEST_DONE 监听",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"tabs",
|
"tabs",
|
||||||
|
"storage",
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
|
|||||||
Reference in New Issue
Block a user