Skip to content

Commit 8e46ed3

Browse files
author
Taois
committed
feat: 增强网页脚本框架
1 parent 886a816 commit 8e46ed3

File tree

1 file changed

+302
-27
lines changed

1 file changed

+302
-27
lines changed

public/monkey/clipboard-sender.user.js

Lines changed: 302 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4718,6 +4718,148 @@
47184718
return { start, stop };
47194719
})();
47204720

4721+
/**
4722+
* CSS 选择器生成工具
4723+
* 提供简易模式和全路径模式
4724+
*/
4725+
4726+
/**
4727+
* 检查选择器在文档中是否唯一
4728+
* @param {string} selector
4729+
* @returns {boolean}
4730+
*/
4731+
function isUnique(selector) {
4732+
try {
4733+
return document.querySelectorAll(selector).length === 1;
4734+
} catch (e) {
4735+
return false;
4736+
}
4737+
}
4738+
4739+
/**
4740+
* 转义 CSS 特殊字符
4741+
* @param {string} str
4742+
* @returns {string}
4743+
*/
4744+
function escape(str) {
4745+
if (typeof CSS !== 'undefined' && CSS.escape) {
4746+
return CSS.escape(str);
4747+
}
4748+
return str.replace(/([!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, '\\$1');
4749+
}
4750+
4751+
/**
4752+
* 生成全路径选择器 (原有逻辑优化)
4753+
* 倾向于使用层级结构: div > div:nth-of-type(2) > span
4754+
* @param {HTMLElement} el
4755+
* @returns {string}
4756+
*/
4757+
function generateFullPathSelector(el) {
4758+
if (el.id) return `#${escape(el.id)}`;
4759+
4760+
const path = [];
4761+
let current = el;
4762+
4763+
while (current && current.nodeType === Node.ELEMENT_NODE) {
4764+
let selector = current.nodeName.toLowerCase();
4765+
4766+
if (current.id) {
4767+
selector = '#' + escape(current.id);
4768+
path.unshift(selector);
4769+
break;
4770+
} else {
4771+
let sib = current, nth = 1;
4772+
while (sib = sib.previousElementSibling) {
4773+
if (sib.nodeName.toLowerCase() == selector)
4774+
nth++;
4775+
}
4776+
if (nth != 1)
4777+
selector += ":nth-of-type("+nth+")";
4778+
}
4779+
path.unshift(selector);
4780+
current = current.parentNode;
4781+
}
4782+
return path.join(" > ");
4783+
}
4784+
4785+
/**
4786+
* 生成简易选择器
4787+
* 优先使用 ID > 唯一Class > 唯一Tag > Tag+Class > 组合Class > 属性 > 短路径
4788+
* @param {HTMLElement} el
4789+
* @returns {string}
4790+
*/
4791+
function generateSimpleSelector(el) {
4792+
const tagName = el.tagName.toLowerCase();
4793+
4794+
// 1. ID
4795+
if (el.id) {
4796+
const selector = `#${escape(el.id)}`;
4797+
if (isUnique(selector)) return selector;
4798+
}
4799+
4800+
// 2. Class handling (improved for SVG support and combinations)
4801+
const classAttr = el.getAttribute('class');
4802+
if (classAttr) {
4803+
const classes = classAttr.split(/\s+/).filter(c => c.trim());
4804+
4805+
// 2.1 Single Class
4806+
for (const cls of classes) {
4807+
const selector = `.${escape(cls)}`;
4808+
if (isUnique(selector)) return selector;
4809+
4810+
// Tag + Single Class
4811+
const tagSelector = `${tagName}${selector}`;
4812+
if (isUnique(tagSelector)) return tagSelector;
4813+
}
4814+
4815+
// 2.2 Two Classes Combination
4816+
if (classes.length >= 2) {
4817+
for (let i = 0; i < classes.length; i++) {
4818+
for (let j = i + 1; j < classes.length; j++) {
4819+
const selector = `.${escape(classes[i])}.${escape(classes[j])}`;
4820+
if (isUnique(selector)) return selector;
4821+
4822+
const tagSelector = `${tagName}${selector}`;
4823+
if (isUnique(tagSelector)) return tagSelector;
4824+
}
4825+
}
4826+
}
4827+
4828+
// 2.3 All Classes (if more than 2)
4829+
if (classes.length > 2) {
4830+
const comboSelector = `.${classes.map(escape).join('.')}`;
4831+
if (isUnique(comboSelector)) return comboSelector;
4832+
const tagComboSelector = `${tagName}${comboSelector}`;
4833+
if (isUnique(tagComboSelector)) return tagComboSelector;
4834+
}
4835+
}
4836+
4837+
// 3. 唯一 Tag
4838+
if (isUnique(tagName)) return tagName;
4839+
4840+
// 4. 常见属性 (name, type, alt, title, aria-label)
4841+
const attrs = ['name', 'type', 'alt', 'title', 'aria-label', 'placeholder', 'data-id', 'data-test-id'];
4842+
for (const attr of attrs) {
4843+
const val = el.getAttribute(attr);
4844+
if (val) {
4845+
const selector = `${tagName}[${attr}="${escape(val)}"]`;
4846+
if (isUnique(selector)) return selector;
4847+
}
4848+
}
4849+
4850+
// 5. 如果是链接,尝试 href
4851+
if (tagName === 'a' && el.href) {
4852+
const href = el.getAttribute('href');
4853+
if (href) {
4854+
const selector = `a[href="${escape(href)}"]`;
4855+
if (isUnique(selector)) return selector;
4856+
}
4857+
}
4858+
4859+
// 6. 降级到全路径
4860+
return generateFullPathSelector(el);
4861+
}
4862+
47214863
const ElementPicker = (() => {
47224864
let active = false;
47234865
let overlay = null;
@@ -4841,34 +4983,13 @@
48414983

48424984
// 生成唯一的 CSS 选择器
48434985
function generateSelector(el) {
4844-
if (el.id) return `#${el.id}`;
4845-
4846-
const path = [];
4847-
let current = el;
4848-
4849-
while (current && current.nodeType === Node.ELEMENT_NODE) {
4850-
let selector = current.nodeName.toLowerCase();
4851-
4852-
if (current.id) {
4853-
selector = '#' + current.id;
4854-
path.unshift(selector);
4855-
break;
4856-
} else {
4857-
let sib = current, nth = 1;
4858-
while (sib = sib.previousElementSibling) {
4859-
if (sib.nodeName.toLowerCase() == selector)
4860-
nth++;
4861-
}
4862-
if (nth != 1)
4863-
selector += ":nth-of-type("+nth+")";
4864-
}
4865-
path.unshift(selector);
4866-
current = current.parentNode;
4867-
4868-
// 限制长度,避免生成的选择器过长
4869-
if (path.length >= 4) break;
4986+
// 0: simple (default), 1: full
4987+
const mode = store.get('element_picker_mode', 0);
4988+
if (mode === 0) {
4989+
return generateSimpleSelector(el);
4990+
} else {
4991+
return generateFullPathSelector(el);
48704992
}
4871-
return path.join(" > ");
48724993
}
48734994

48744995
return { start, stop };
@@ -4898,6 +5019,132 @@
48985019
setGrayMode(enabled);
48995020
}
49005021

5022+
let styleEl = null;
5023+
5024+
// 针对特定事件的处理器,确保允许默认行为
5025+
const allowHandler = (e) => {
5026+
e.stopImmediatePropagation(); // 阻止其他监听器执行
5027+
return true;
5028+
};
5029+
5030+
function setUnlockCopy(enable) {
5031+
// 定义需要拦截的事件列表
5032+
const events = ['copy', 'cut', 'paste', 'selectstart', 'contextmenu', 'dragstart', 'mousedown', 'mouseup', 'keydown', 'keyup'];
5033+
5034+
if (enable) {
5035+
// 1. 注入 CSS
5036+
if (!styleEl) {
5037+
styleEl = document.createElement('style');
5038+
styleEl.innerHTML = `
5039+
* {
5040+
-webkit-user-select: text !important;
5041+
-moz-user-select: text !important;
5042+
-ms-user-select: text !important;
5043+
user-select: text !important;
5044+
}
5045+
`;
5046+
document.head.appendChild(styleEl);
5047+
}
5048+
5049+
// 2. 拦截事件
5050+
// 使用捕获阶段,阻止事件向下一级传播,防止网页JS拦截
5051+
events.forEach(evt => {
5052+
window.addEventListener(evt, allowHandler, true);
5053+
document.addEventListener(evt, allowHandler, true);
5054+
});
5055+
5056+
// 3. 定时清理内联属性 (暴力清除)
5057+
window._unlockTimer = setInterval(() => {
5058+
try {
5059+
const targets = [document, document.body];
5060+
const props = ['oncopy', 'oncut', 'onpaste', 'onselectstart', 'oncontextmenu'];
5061+
targets.forEach(t => {
5062+
if(!t) return;
5063+
props.forEach(p => {
5064+
t[p] = null;
5065+
});
5066+
});
5067+
} catch(e) {}
5068+
}, 1000);
5069+
5070+
Toast.show('提示', '已开启解除复制限制\n现在可以尝试选择文本并复制');
5071+
} else {
5072+
// 1. 移除 CSS
5073+
if (styleEl) {
5074+
styleEl.remove();
5075+
styleEl = null;
5076+
}
5077+
5078+
// 2. 移除事件监听
5079+
events.forEach(evt => {
5080+
window.removeEventListener(evt, allowHandler, true);
5081+
document.removeEventListener(evt, allowHandler, true);
5082+
});
5083+
5084+
// 3. 清除定时器
5085+
if (window._unlockTimer) {
5086+
clearInterval(window._unlockTimer);
5087+
window._unlockTimer = null;
5088+
}
5089+
5090+
Toast.show('提示', '已关闭解除复制限制');
5091+
}
5092+
}
5093+
5094+
5095+
5096+
// 强制复制选中文本
5097+
async function forceCopySelection() {
5098+
const selection = window.getSelection();
5099+
const text = selection.toString();
5100+
5101+
if (!text) {
5102+
Toast.show('提示', '请先选择要复制的文本');
5103+
return;
5104+
}
5105+
5106+
// 优先使用 GM_setClipboard (它不依赖页面事件,也不受拦截器影响)
5107+
if (typeof GM_setClipboard !== 'undefined') {
5108+
try {
5109+
GM_setClipboard(text, 'text');
5110+
Toast.show('成功', '已强制复制选中内容到剪切板');
5111+
return;
5112+
} catch (e) {
5113+
console.error('GM_setClipboard 失败,降级尝试:', e);
5114+
}
5115+
}
5116+
5117+
// 如果必须使用 navigator.clipboard.writeText
5118+
// 临时禁用拦截器,确保复制操作能通过
5119+
const wasEnabled = styleEl !== null; // 通过 styleEl 判断是否开启
5120+
const events = ['copy', 'cut', 'paste', 'selectstart', 'contextmenu', 'dragstart', 'mousedown', 'mouseup', 'keydown', 'keyup'];
5121+
5122+
try {
5123+
if (wasEnabled) {
5124+
events.forEach(evt => {
5125+
window.removeEventListener(evt, allowHandler, true);
5126+
document.removeEventListener(evt, allowHandler, true);
5127+
});
5128+
}
5129+
5130+
await navigator.clipboard.writeText(text);
5131+
Toast.show('成功', '已复制选中内容');
5132+
5133+
} catch (err) {
5134+
console.error('复制失败:', err);
5135+
Toast.show('错误', '复制失败: ' + err.message);
5136+
} finally {
5137+
// 恢复拦截器 (放在 finally 块中确保总是执行)
5138+
// 重新检查 styleEl 状态,因为在此期间可能被关闭
5139+
if (styleEl !== null) {
5140+
events.forEach(evt => {
5141+
window.addEventListener(evt, allowHandler, true);
5142+
document.addEventListener(evt, allowHandler, true);
5143+
});
5144+
}
5145+
}
5146+
}
5147+
49015148
function toggleLog() {
49025149
// 使用store中的状态作为主要判断依据,DOM状态作为备用
49035150
const storedHidden = store.get('logger.hidden', 0) === 1;
@@ -5196,6 +5443,17 @@
51965443
Logger.append(`[开关集] ${label}`);
51975444
}
51985445

5446+
function toggleUnlockCopy(enable) {
5447+
setUnlockCopy(enable);
5448+
const label = enable ? '开复制' : '关复制';
5449+
Logger.append(`[开关集] ${label}`);
5450+
}
5451+
5452+
function copySelection() {
5453+
forceCopySelection();
5454+
Logger.append(`[工具] 强制复制选中文本`);
5455+
}
5456+
51995457
async function configSafeCode() {
52005458
const key = 'clipboard_safecode';
52015459
const current = store.get(key, '');
@@ -5622,6 +5880,7 @@
56225880

56235881
// 第4列:定时任务、推送文本
56245882
{ id: 'schedule-open', label: '定时任务', column: 4, handler: toggleScheduleManager },
5883+
{ id: 'force-copy', label: '复制选中', column: 4, handler: copySelection },
56255884

56265885
// 第5列:配置集、开关集、指令集
56275886
{ id: 'cfg-open', label: '配置集', column: 5, handler: toggleGroup('配置集') },
@@ -5653,6 +5912,14 @@
56535912
storeKey: 'gray_mode_enabled',
56545913
handler: makeToggle('gray-mode', '开灰度', '关灰度', 'gray_mode_enabled', (enabled) => toggleGrayMode(enabled))
56555914
},
5915+
{
5916+
id: 'unlock-copy',
5917+
label: '开复制',
5918+
group: '开关集',
5919+
isToggle: true,
5920+
storeKey: 'unlock_copy_enabled',
5921+
handler: makeToggle('unlock-copy', '开复制', '关复制', 'unlock_copy_enabled', (enabled) => toggleUnlockCopy(enabled))
5922+
},
56565923

56575924
// 分组:配置集
56585925
{
@@ -5681,6 +5948,14 @@
56815948
storeKey: 'remote_commands_enabled',
56825949
handler: makeToggle('cfg-remote-enable', '启用远程指令', '禁用远程指令', 'remote_commands_enabled')
56835950
},
5951+
{
5952+
id: 'cfg-picker-mode',
5953+
label: '模式: 简易',
5954+
group: '配置集',
5955+
isToggle: true,
5956+
storeKey: 'element_picker_mode',
5957+
handler: makeToggle('cfg-picker-mode', '模式: 简易', '模式: 全路径', 'element_picker_mode')
5958+
},
56845959
// 组内按钮:推送文本
56855960
{
56865961
id: 'cfg-push',

0 commit comments

Comments
 (0)