- Detect await usage in sync context - Auto-wrap with async function - Return friendly prompt suggesting to avoid await - Provide alternative solution using global variables
429 lines
15 KiB
JavaScript
429 lines
15 KiB
JavaScript
// ==UserScript==
|
|
// @name ljq_web_driver
|
|
// @namespace http://tampermonkey.net/
|
|
// @version 0.32
|
|
// @description Execute JS via ljq_web_driver
|
|
// @require https://code.jquery.com/jquery-3.6.0.min.js
|
|
// @author You
|
|
// @match *://*/*
|
|
// @grant GM_setValue
|
|
// @grant GM_getValue
|
|
// @grant GM_xmlhttpRequest
|
|
// @grant GM_openInTab
|
|
// @grant unsafeWindow
|
|
// @connect localhost
|
|
// @run-at document-start
|
|
// ==/UserScript==
|
|
|
|
|
|
(function() {
|
|
'use strict';
|
|
const log_prefix = "ljq_driver: ";
|
|
|
|
if (window.self !== window.top) {
|
|
window.addEventListener('message',e=>{if(e.data?.type==='ljq_exec'){try{let r=eval(e.data.code);parent.postMessage({type:'ljq_result',id:e.data.id,result:String(r)},'*')}catch(err){parent.postMessage({type:'ljq_result',id:e.data.id,error:err.message},'*')}}});
|
|
return;
|
|
}
|
|
|
|
const wsUrl = 'ws://localhost:18765';
|
|
const httpUrl = 'http://localhost:18766/';
|
|
|
|
function isWebSocketServerAlive(callback) {
|
|
GM_xmlhttpRequest({
|
|
method: 'GET',
|
|
url: 'http://localhost:18765/',
|
|
onload: () => callback(true),
|
|
onerror: () => callback(false)
|
|
});
|
|
}
|
|
|
|
let ws;
|
|
let sid;
|
|
if (window.opener && window.name && window.name.startsWith('ljq_')) {
|
|
sid = null;
|
|
console.log(log_prefix + `检测到opener,丢弃继承的window.name: ${window.name}`);
|
|
window.name = '';
|
|
} else {
|
|
sid = (window.name && window.name.startsWith('ljq_')) ?
|
|
window.name : window.sessionStorage.getItem('ljq_driver_sid');
|
|
}
|
|
if (!sid) {
|
|
sid = `ljq_${Date.now().toString().slice(-2)}${Math.random().toString(36).slice(2, 4)}`;
|
|
window.sessionStorage.setItem('ljq_driver_sid', sid);
|
|
window.name = sid;
|
|
console.log(log_prefix + `创建新会话ID: ${sid}`);
|
|
} else {
|
|
if (window.name !== sid) window.name = sid;
|
|
console.log(log_prefix + `使用现有会话ID: ${sid}`);
|
|
}
|
|
|
|
// 保存会话ID
|
|
GM_setValue('sid', sid);
|
|
|
|
// 获取或创建状态指示器
|
|
function getIndicator() {
|
|
// 检查现有指示器
|
|
let ind = document.getElementById('ljq-ind');
|
|
|
|
// 删除重复指示器
|
|
const dups = document.querySelectorAll('[id="ljq-ind"]');
|
|
if (dups.length > 1) {
|
|
for (let i = 1; i < dups.length; i++) {
|
|
dups[i].remove();
|
|
}
|
|
ind = dups[0];
|
|
}
|
|
|
|
// 创建新指示器
|
|
if (!ind && document.body) {
|
|
ind = document.createElement('div');
|
|
ind.id = 'ljq-ind';
|
|
ind.style.cssText = `
|
|
position: fixed;bottom: 10px;
|
|
right: 10px;background-color: #f44336;
|
|
color: white;padding: 8px 12px;
|
|
border-radius: 6px;font-size: 14px;
|
|
font-weight: bold;z-index: 9999;
|
|
transition: background-color 0.3s;
|
|
cursor: pointer;box-shadow: 0 3px 6px rgba(0,0,0,0.25);
|
|
`;
|
|
ind.innerText = log_prefix + '正在连接...';
|
|
|
|
ind.addEventListener('click', () => alert(`会话ID: ${sid}\n当前URL: ${location.href}`));
|
|
document.body.appendChild(ind);
|
|
}
|
|
|
|
return ind;
|
|
}
|
|
|
|
// 更新状态
|
|
function updateStatus(status, msg) {
|
|
if (!document.body) return setTimeout(() => updateStatus(status, msg), 100);
|
|
|
|
const ind = getIndicator();
|
|
if (!ind) return;
|
|
|
|
if (status === 'ok') {
|
|
ind.style.backgroundColor = '#4CAF50';
|
|
ind.innerText = log_prefix + '连接成功';
|
|
} else if (status === 'disc') {
|
|
ind.style.backgroundColor = '#f44336';
|
|
ind.innerText = log_prefix + '连接断开';
|
|
} else if (status === 'conn') {
|
|
ind.style.backgroundColor = '#2196F3';
|
|
ind.innerText = log_prefix + '正在连接(HTTP)';
|
|
} else if (status === 'err') {
|
|
ind.style.backgroundColor = '#FF9800';
|
|
ind.innerText = log_prefix + `发生错误 (${msg})`;
|
|
} else if (status === 'exec') {
|
|
ind.style.backgroundColor = '#2196F3';
|
|
ind.innerText = log_prefix + '正在执行指令...';
|
|
}
|
|
}
|
|
|
|
function handleError(id, error, errorSource) {
|
|
console.error(`${errorSource}错误:`, error);
|
|
updateStatus('err', error.message);
|
|
|
|
const errorMessage = {
|
|
type: 'error',
|
|
id: id,
|
|
sessionId: sid,
|
|
error: {
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: error.stack,
|
|
source: errorSource
|
|
}
|
|
};
|
|
|
|
if (typeof ws !== 'undefined' && ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(errorMessage));
|
|
} else {
|
|
GM_xmlhttpRequest({
|
|
method: "POST",
|
|
url: httpUrl + "api/result",
|
|
headers: {"Content-Type": "application/json"},
|
|
data: JSON.stringify(errorMessage),
|
|
onload: function(response) {console.log("错误信息已通过HTTP发送", response);},
|
|
onerror: function(err) {console.error("发送错误信息失败", err);}
|
|
});
|
|
}
|
|
}
|
|
|
|
function smartProcessResult(result) {
|
|
// 处理 null 和原始类型
|
|
if (result === null || result === undefined || typeof result !== 'object') {
|
|
return result;
|
|
}
|
|
|
|
// 1. 处理 jQuery 对象 - 强制转换为HTML字符串数组
|
|
if (typeof jQuery !== 'undefined' && result instanceof jQuery) {
|
|
const elements = [];
|
|
for (let i = 0; i < result.length; i++) {
|
|
if (result[i] && result[i].nodeType === 1) {
|
|
elements.push(result[i].outerHTML);
|
|
}
|
|
}
|
|
return elements; // 始终返回数组
|
|
}
|
|
|
|
// 2. 处理 NodeList 和 HTMLCollection
|
|
if (result instanceof NodeList || result instanceof HTMLCollection) {
|
|
const elements = [];
|
|
for (let i = 0; i < result.length; i++) {
|
|
if (result[i] && result[i].nodeType === 1) {
|
|
elements.push(result[i].outerHTML);
|
|
}
|
|
}
|
|
return elements;
|
|
}
|
|
|
|
// 3. 处理单个 DOM 元素
|
|
if (result.nodeType === 1) {
|
|
return result.outerHTML;
|
|
}
|
|
|
|
// 4. 检查是否是具有数字索引和length属性的类数组对象
|
|
if (!Array.isArray(result) &&
|
|
typeof result === 'object' &&
|
|
'length' in result &&
|
|
typeof result.length === 'number') {
|
|
|
|
// 检查第一个元素是否是DOM节点
|
|
const firstElement = result[0];
|
|
if (firstElement && firstElement.nodeType === 1) {
|
|
const elements = [];
|
|
const length = Math.min(result.length, 100);
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
const elem = result[i];
|
|
if (elem && elem.nodeType === 1) {
|
|
elements.push(elem.outerHTML);
|
|
}
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
}
|
|
|
|
// 5. 处理普通对象和数组 - 使用标准序列化
|
|
try {
|
|
return JSON.parse(JSON.stringify(result, function(key, value) {
|
|
if (typeof value === 'object' && value !== null) {
|
|
if (value.nodeType === 1) {
|
|
return value.outerHTML;
|
|
}
|
|
if (value === window || value === document) {
|
|
return '[Object]';
|
|
}
|
|
}
|
|
return value;
|
|
}));
|
|
} catch (e) {
|
|
console.error("序列化对象失败:", e);
|
|
return `[无法序列化的对象: ${e.message}]`;
|
|
}
|
|
}
|
|
|
|
// 防止重复初始化
|
|
if (window.ljq_init) return;
|
|
window.ljq_init = true;
|
|
|
|
function connecthttp() {
|
|
if (window.use_ws) return;
|
|
updateStatus('conn');
|
|
GM_xmlhttpRequest({
|
|
method: "POST",
|
|
url: httpUrl + "api/longpoll",
|
|
headers: {"Content-Type": "application/json"},
|
|
data: JSON.stringify({
|
|
type: 'ready',
|
|
url: location.href,
|
|
sessionId: sid
|
|
}),
|
|
onload: function(resp) {
|
|
if (resp.status === 200) {
|
|
let data = JSON.parse(resp.responseText);
|
|
console.log(log_prefix + '接收到数据:', data);
|
|
if (data.id === "" && data.ret === "use ws") return;
|
|
if (data.id === "") return setTimeout(connecthttp, 100);
|
|
const response = executeCode(data);
|
|
|
|
if (response.error) {
|
|
handleError(data.id, response.error, '执行代码');
|
|
} else {
|
|
GM_xmlhttpRequest({
|
|
method: "POST",
|
|
url: httpUrl + "api/result",
|
|
headers: {"Content-Type": "application/json"},
|
|
data: JSON.stringify({
|
|
type: 'result',
|
|
id: data.id,
|
|
sessionId: sid,
|
|
result: response.result
|
|
})
|
|
});
|
|
}
|
|
} else {
|
|
console.error(log_prefix + '请求失败,状态码:', resp.status);
|
|
updateStatus('err', '请求失败');
|
|
}
|
|
setTimeout(connecthttp, 1000);
|
|
},
|
|
onerror: function(err) {
|
|
console.error(log_prefix + '请求错误', err);
|
|
updateStatus('err', '请求失败');
|
|
setTimeout(connecthttp, 5000);
|
|
},
|
|
ontimeout: function() {
|
|
console.log(log_prefix + '请求超时');
|
|
updateStatus('err', '请求超时');
|
|
setTimeout(connecthttp, 5000);
|
|
}
|
|
});
|
|
}
|
|
|
|
function executeCode(data) {
|
|
let id = data.id || 'unknown'; // 获取 ID
|
|
let result;
|
|
|
|
if (!data.code) {
|
|
console.log('收到非代码执行消息:', data);
|
|
return { error: '没有可执行的代码' };
|
|
}
|
|
updateStatus('exec');
|
|
const _open = window.open;
|
|
window.open = (url, target, features) => {
|
|
GM_openInTab(url, { active: true });
|
|
return { success: true, url: url };
|
|
};
|
|
try {
|
|
const jsCode = data.code.trim();
|
|
const lines = jsCode.split(/\r?\n/).filter(l => l.trim());
|
|
const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : '';
|
|
if (lastLine.startsWith('return')) {
|
|
result = (new Function(jsCode))();
|
|
} else {
|
|
try {
|
|
result = eval(jsCode);
|
|
} catch (e) {
|
|
if (isIllegalReturnError(e)) {
|
|
result = (new Function(jsCode))();
|
|
} else if (isAwaitError(e)) {
|
|
result = (async function() { return eval(jsCode); })();
|
|
result = 'Promise is running, cannot get return value. Suggest avoiding await next time, or use global variables (e.g., window.myVar) to store async results.';
|
|
} else throw e;
|
|
}
|
|
}
|
|
const processedResult = smartProcessResult(result);
|
|
if (result instanceof Promise) {
|
|
result.finally(() => window.open = _open);
|
|
return { result: processedResult };
|
|
}
|
|
return { result: processedResult };
|
|
} catch (execError) {
|
|
return { error: execError };
|
|
} finally {
|
|
if (!(result instanceof Promise)) {
|
|
setTimeout(() => window.open = _open, 100);
|
|
}
|
|
}
|
|
}
|
|
|
|
function isIllegalReturnError(e) {
|
|
return e instanceof SyntaxError && (
|
|
/Illegal return statement/i.test(e.message) || // Chrome 常见
|
|
/return not in function/i.test(e.message) || // Firefox 常见
|
|
/Illegal 'return' statement/i.test(e.message) // 兼容旧文案
|
|
);
|
|
}
|
|
|
|
function isAwaitError(e) {
|
|
return e instanceof SyntaxError && (
|
|
/await is only valid in async/i.test(e.message) || // Chrome
|
|
/await.*async/i.test(e.message) // Firefox等
|
|
);
|
|
}
|
|
|
|
function connect() {
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = function() {
|
|
window.use_ws = true;
|
|
console.log(log_prefix + '已连接');
|
|
updateStatus('ok');
|
|
ws.send(JSON.stringify({
|
|
type: 'ready',
|
|
url: location.href,
|
|
sessionId: sid
|
|
}));
|
|
};
|
|
|
|
ws.onclose = function() {
|
|
console.log(log_prefix + '已断开,5秒后重连');
|
|
updateStatus('disc');
|
|
setTimeout(connect, 5000);
|
|
};
|
|
|
|
ws.onerror = function(err) {
|
|
console.error(log_prefix + '连接错误', err);
|
|
updateStatus('err', '连接失败');
|
|
isWebSocketServerAlive(function (e) { if (e) connecthttp()});
|
|
};
|
|
|
|
ws.onmessage = async function(e) {
|
|
try {
|
|
let data = JSON.parse(e.data);
|
|
ws.send(JSON.stringify({type: 'ack',id: data.id}));
|
|
const response = executeCode(data);
|
|
|
|
if (response.error) {
|
|
handleError(data.id, response.error, '执行代码');
|
|
} else {
|
|
updateStatus('ok');
|
|
ws.send(JSON.stringify({
|
|
type: 'result',
|
|
id: data.id,
|
|
sessionId: sid,
|
|
result: response.result
|
|
}));
|
|
}
|
|
} catch (parseError) {
|
|
handleError('unknown', parseError, '解析消息');
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
// 初始化
|
|
function init() {
|
|
if (document.body) {
|
|
getIndicator();
|
|
connect();
|
|
} else {
|
|
setTimeout(init, 50);
|
|
}
|
|
}
|
|
|
|
// 监控DOM变化
|
|
const observer = new MutationObserver(() => getIndicator());
|
|
|
|
if (document.readyState !== 'loading') {
|
|
init();
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
init();
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
});
|
|
}
|
|
|
|
// 清理
|
|
window.addEventListener('beforeunload', () => {
|
|
observer.disconnect();
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.close();
|
|
}
|
|
});
|
|
})(); |