cleanup: rm user.js, untrack copilot_proxy.pyw; ljqCtrl: DPI via GDI + GrabWindow(hwnd)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -75,6 +75,7 @@ restore_commit.txt
|
||||
sche_tasks/
|
||||
# CDP Bridge 密钥配置(首次运行自动生成)
|
||||
assets/tmwd_cdp_bridge/config.js
|
||||
assets/copilot_proxy.pyw
|
||||
**log.*
|
||||
|
||||
# Reflect (ignore new files, whitelist existing)
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
"""
|
||||
Copilot Local Proxy - TK GUI
|
||||
本地 OpenAI 兼容代理,自动管理 Copilot token 并转发请求
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import scrolledtext
|
||||
import threading, json, os, time, uuid
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
import requests
|
||||
|
||||
# ============ Config ============
|
||||
OAUTH_PATH = os.path.join(os.path.expanduser('~'), '.copilot_oauth.json')
|
||||
COPILOT_TOKEN_URL = 'https://api.github.com/copilot_internal/v2/token'
|
||||
COPILOT_API_BASE = 'https://api.githubcopilot.com'
|
||||
PROXY = {'https': 'http://127.0.0.1:2082'}
|
||||
LOCAL_PORT = 15432
|
||||
REFRESH_MARGIN = 120 # 提前120秒刷新
|
||||
|
||||
COPILOT_HEADERS = {
|
||||
'Editor-Version': 'vscode/1.110.1',
|
||||
'Editor-Plugin-Version': 'copilot-chat/0.38.2',
|
||||
'User-Agent': 'GitHubCopilotChat/0.38.2',
|
||||
'Copilot-Integration-Id': 'vscode-chat',
|
||||
'openai-intent': 'conversation-panel',
|
||||
}
|
||||
|
||||
|
||||
# ============ Token Manager ============
|
||||
class TokenManager:
|
||||
def __init__(self, log_fn=print):
|
||||
self.copilot_token = None
|
||||
self.expires_at = 0
|
||||
self.log = log_fn
|
||||
self._lock = threading.Lock()
|
||||
with open(OAUTH_PATH) as f:
|
||||
self.access_token = json.load(f)['access_token']
|
||||
self.log(f"[Token] OAuth token loaded: ***{self.access_token[-6:]}")
|
||||
|
||||
def get_token(self):
|
||||
with self._lock:
|
||||
if time.time() < self.expires_at - REFRESH_MARGIN:
|
||||
return self.copilot_token
|
||||
return self._refresh()
|
||||
|
||||
def _refresh(self):
|
||||
self.log("[Token] Refreshing copilot token...")
|
||||
try:
|
||||
resp = requests.get(COPILOT_TOKEN_URL, headers={
|
||||
'Authorization': f'token {self.access_token}',
|
||||
'User-Agent': 'GitHubCopilotChat/0.38.2',
|
||||
'Accept': 'application/json',
|
||||
}, proxies=PROXY, timeout=15)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
self.copilot_token = data['token']
|
||||
self.expires_at = data['expires_at']
|
||||
remain = int(self.expires_at - time.time())
|
||||
self.log(f"[Token] Refreshed OK, expires in {remain}s")
|
||||
return self.copilot_token
|
||||
except Exception as e:
|
||||
self.log(f"[Token] Refresh FAILED: {e}")
|
||||
return self.copilot_token
|
||||
|
||||
|
||||
# ============ Proxy Handler ============
|
||||
class ProxyHandler(BaseHTTPRequestHandler):
|
||||
token_mgr: TokenManager = None
|
||||
log_fn = print
|
||||
|
||||
def do_POST(self):
|
||||
try:
|
||||
length = int(self.headers.get('Content-Length', 0))
|
||||
body = json.loads(self.rfile.read(length)) if length else {}
|
||||
model = body.get('model', '?')
|
||||
stream = body.get('stream', False)
|
||||
self.log_fn(f"[Req] {model} stream={stream}")
|
||||
|
||||
token = self.token_mgr.get_token()
|
||||
if not token:
|
||||
self._error(503, "No copilot token available")
|
||||
return
|
||||
|
||||
headers = {**COPILOT_HEADERS,
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Content-Type': 'application/json',
|
||||
'x-request-id': str(uuid.uuid4())}
|
||||
|
||||
path = self.path
|
||||
if path.startswith('/v1/'):
|
||||
path = path[3:] # strip /v1 prefix
|
||||
target = f"{COPILOT_API_BASE}{path}"
|
||||
resp = requests.post(target, headers=headers, json=body,
|
||||
proxies=PROXY, timeout=120, stream=stream)
|
||||
|
||||
if stream and 'text/event-stream' in resp.headers.get('content-type', ''):
|
||||
self.send_response(resp.status_code)
|
||||
self.send_header('Content-Type', 'text/event-stream')
|
||||
self.send_header('Cache-Control', 'no-cache')
|
||||
self.end_headers()
|
||||
for chunk in resp.iter_content(chunk_size=None):
|
||||
if chunk:
|
||||
self.wfile.write(chunk)
|
||||
self.wfile.flush()
|
||||
else:
|
||||
self.send_response(resp.status_code)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(resp.content)
|
||||
|
||||
self.log_fn(f"[Resp] {resp.status_code}")
|
||||
except Exception as e:
|
||||
self.log_fn(f"[Error] {e}")
|
||||
self._error(502, str(e))
|
||||
|
||||
def do_GET(self):
|
||||
try:
|
||||
token = self.token_mgr.get_token()
|
||||
headers = {**COPILOT_HEADERS, 'Authorization': f'Bearer {token}'}
|
||||
path = self.path
|
||||
if path.startswith('/v1/'):
|
||||
path = path[3:]
|
||||
target = f"{COPILOT_API_BASE}{path}"
|
||||
resp = requests.get(target, headers=headers, proxies=PROXY, timeout=15)
|
||||
self.send_response(resp.status_code)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(resp.content)
|
||||
except Exception as e:
|
||||
self._error(502, str(e))
|
||||
|
||||
def _error(self, code, msg):
|
||||
self.send_response(code)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'error': msg}).encode())
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
pass
|
||||
|
||||
|
||||
# ============ TK GUI ============
|
||||
class App:
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("Copilot Proxy")
|
||||
self.root.geometry("520x360")
|
||||
self.root.resizable(False, False)
|
||||
|
||||
frm = tk.Frame(self.root)
|
||||
frm.pack(fill='x', padx=8, pady=4)
|
||||
self.status_var = tk.StringVar(value="Starting...")
|
||||
tk.Label(frm, textvariable=self.status_var, fg='blue', anchor='w').pack(side='left')
|
||||
tk.Label(frm, text=f":{LOCAL_PORT}", fg='gray').pack(side='right')
|
||||
|
||||
self.log_area = scrolledtext.ScrolledText(
|
||||
self.root, height=18, state='disabled', font=('Consolas', 9))
|
||||
self.log_area.pack(fill='both', expand=True, padx=8, pady=4)
|
||||
|
||||
self.token_mgr = TokenManager(log_fn=self.log)
|
||||
threading.Thread(target=self._run_server, daemon=True).start()
|
||||
|
||||
def log(self, msg):
|
||||
ts = time.strftime('%H:%M:%S')
|
||||
def _append():
|
||||
self.log_area.config(state='normal')
|
||||
self.log_area.insert('end', f"[{ts}] {msg}\n")
|
||||
self.log_area.see('end')
|
||||
self.log_area.config(state='disabled')
|
||||
self.root.after(0, _append)
|
||||
|
||||
def _run_server(self):
|
||||
ProxyHandler.token_mgr = self.token_mgr
|
||||
ProxyHandler.log_fn = self.log
|
||||
server = HTTPServer(('127.0.0.1', LOCAL_PORT), ProxyHandler)
|
||||
self.log(f"[Server] Listening on http://127.0.0.1:{LOCAL_PORT}")
|
||||
self.root.after(0, lambda: self.status_var.set(f"Running 127.0.0.1:{LOCAL_PORT}"))
|
||||
self.token_mgr.get_token()
|
||||
server.serve_forever()
|
||||
|
||||
def run(self):
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
App().run()
|
||||
@@ -1,422 +0,0 @@
|
||||
// ==UserScript==
|
||||
// @name ljq_web_driver
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 0.40
|
||||
// @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 127.0.0.1
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
const log_prefix = "ljq_driver: ";
|
||||
if (document.querySelector('[data-testid="stApp"],.stApp')) return;
|
||||
if (/<title>\s*Streamlit\s*<\/title>|window\.prerenderReady=!1|You need to enable JavaScript to run this app\./i.test(document.documentElement?.outerHTML || '')) return;
|
||||
|
||||
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://127.0.0.1:18765';
|
||||
const httpUrl = 'http://127.0.0.1:18766/';
|
||||
|
||||
function isWebSocketServerAlive(callback) {
|
||||
GM_xmlhttpRequest({
|
||||
method: 'GET',
|
||||
url: 'http://127.0.0.1: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 : null;
|
||||
}
|
||||
if (!sid) {
|
||||
sid = `ljq_${Date.now().toString().slice(-2)}${Math.random().toString(36).slice(2, 4)}`;
|
||||
window.name = sid;
|
||||
console.log(log_prefix + `创建新会话ID: ${sid}`);
|
||||
} else {
|
||||
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: async 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 = await 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async 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() : '';
|
||||
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
||||
|
||||
if (lastLine.startsWith('return')) {
|
||||
result = await (new AsyncFunction(jsCode))();
|
||||
} else {
|
||||
try {
|
||||
result = eval(jsCode);
|
||||
if (result instanceof Promise) result = await result;
|
||||
} catch (e) {
|
||||
if (isIllegalReturnError(e) || isAwaitError(e)) {
|
||||
result = await (new AsyncFunction(jsCode))();
|
||||
} else throw e;
|
||||
}
|
||||
}
|
||||
const processedResult = smartProcessResult(result);
|
||||
return { result: processedResult };
|
||||
} catch (execError) {
|
||||
return { error: execError };
|
||||
} finally {
|
||||
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 = await 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变化 (改为10秒定时器以优化性能)
|
||||
let indicatorTimer = null;
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
init();
|
||||
indicatorTimer = setInterval(() => getIndicator(), 10000);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
indicatorTimer = setInterval(() => getIndicator(), 10000);
|
||||
});
|
||||
}
|
||||
|
||||
// 清理
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (indicatorTimer) clearInterval(indicatorTimer);
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -7,9 +7,10 @@ ljqCtrl Quick Reference:
|
||||
- Press(cmd, staytime=0): Keyboard shortcuts (e.g. 'ctrl+v')
|
||||
- FindBlock(fn, wrect=None, threshold=0.8) -> (obj_center_phys, is_found)
|
||||
- MouseDClick(staytime=0.05), MouseClick(staytime=0.05)
|
||||
- GrabWindow(hwnd) -> PIL Image: DPI-safe window screenshot
|
||||
"""
|
||||
|
||||
import os, sys, time, random, math, win32api, win32con
|
||||
import os, sys, time, random, math, win32api, win32con, ctypes
|
||||
import numpy as np
|
||||
|
||||
dpi_scale = 1
|
||||
@@ -18,24 +19,15 @@ try:
|
||||
import cv2
|
||||
except: pass
|
||||
|
||||
try:
|
||||
scr = ImageGrab.grab()
|
||||
swidth, sheight = scr.size
|
||||
print('Screen width & height:', swidth, sheight)
|
||||
cwidth, cheight = map(win32api.GetSystemMetrics, [win32con.SM_CXSCREEN, win32con.SM_CYSCREEN])
|
||||
dpi_scale = cwidth / swidth
|
||||
print('dpi_scale:', dpi_scale)
|
||||
except:
|
||||
import ctypes
|
||||
user32 = ctypes.windll.user32
|
||||
user32.SetProcessDPIAware() # 确保 DPI 感知
|
||||
cwidth = user32.GetSystemMetrics(0) # SM_CXSCREEN
|
||||
cheight = user32.GetSystemMetrics(1) # SM_CYSCREEN
|
||||
try:
|
||||
dpi = user32.GetDpiForSystem() # Windows 10 1607+
|
||||
dpi_scale = dpi / 96.0
|
||||
except: dpi_scale = 1.0 # 降级方案
|
||||
print(f'Screen (RDP disconnected): {cwidth}x{cheight}, dpi_scale: {dpi_scale}')
|
||||
_hdc = ctypes.windll.user32.GetDC(0)
|
||||
swidth = ctypes.windll.gdi32.GetDeviceCaps(_hdc, 118) # DESKTOPHORZRES (物理)
|
||||
sheight = ctypes.windll.gdi32.GetDeviceCaps(_hdc, 117) # DESKTOPVERTRES
|
||||
ctypes.windll.user32.ReleaseDC(0, _hdc)
|
||||
cwidth = win32api.GetSystemMetrics(win32con.SM_CXSCREEN) # 逻辑
|
||||
cheight = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
|
||||
dpi_scale = cwidth / swidth
|
||||
print('Screen width & height:', swidth, sheight)
|
||||
print('dpi_scale:', dpi_scale)
|
||||
|
||||
def MouseDown(): win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)
|
||||
def MouseUp(): win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)
|
||||
@@ -74,6 +66,11 @@ press = Press
|
||||
VK_CODE = {'backspace':0x08, 'tab':0x09, 'clear':0x0C, 'enter':0x0D, 'shift':0x10, 'ctrl':0x11, 'alt':0x12, 'pause':0x13, 'caps_lock':0x14, 'esc':0x1B, 'escape':0x1B, 'space':0x20, 'page_up':0x21, 'page_down':0x22, 'end':0x23, 'home':0x24, 'left_arrow':0x25, 'up_arrow':0x26, 'right_arrow':0x27, 'down_arrow':0x28, 'select':0x29, 'print':0x2A, 'execute':0x2B, 'print_screen':0x2C, 'ins':0x2D, 'del':0x2E, 'help':0x2F, '0':0x30, '1':0x31, '2':0x32, '3':0x33, '4':0x34, '5':0x35, '6':0x36, '7':0x37, '8':0x38, '9':0x39, 'a':0x41, 'b':0x42, 'c':0x43, 'd':0x44, 'e':0x45, 'f':0x46, 'g':0x47, 'h':0x48, 'i':0x49, 'j':0x4A, 'k':0x4B, 'l':0x4C, 'm':0x4D, 'n':0x4E, 'o':0x4F, 'p':0x50, 'q':0x51, 'r':0x52, 's':0x53, 't':0x54, 'u':0x55, 'v':0x56, 'w':0x57, 'x':0x58, 'y':0x59, 'z':0x5A, 'numpad_0':0x60, 'numpad_1':0x61, 'numpad_2':0x62, 'numpad_3':0x63, 'numpad_4':0x64, 'numpad_5':0x65, 'numpad_6':0x66, 'numpad_7':0x67, 'numpad_8':0x68, 'numpad_9':0x69, 'multiply_key':0x6A, 'add_key':0x6B, 'separator_key':0x6C, 'subtract_key':0x6D, 'decimal_key':0x6E, 'divide_key':0x6F, 'F1':0x70, 'F2':0x71, 'F3':0x72, 'F4':0x73, 'F5':0x74, 'F6':0x75, 'F7':0x76, 'F8':0x77, 'F9':0x78, 'F10':0x79, 'F11':0x7A, 'F12':0x7B, 'F13':0x7C, 'F14':0x7D, 'F15':0x7E, 'F16':0x7F, 'F17':0x80, 'F18':0x81, 'F19':0x82, 'F20':0x83, 'F21':0x84, 'F22':0x85, 'F23':0x86, 'F24':0x87, 'num_lock':0x90, 'scroll_lock':0x91, 'left_shift':0xA0, 'right_shift ':0xA1, 'left_control':0xA2, 'right_control':0xA3, 'left_menu':0xA4, 'right_menu':0xA5, 'browser_back':0xA6, 'browser_forward':0xA7, 'browser_refresh':0xA8, 'browser_stop':0xA9, 'browser_search':0xAA, 'browser_favorites':0xAB, 'browser_start_and_home':0xAC, 'volume_mute':0xAD, 'volume_Down':0xAE, 'volume_up':0xAF, 'next_track':0xB0, 'previous_track':0xB1, 'stop_media':0xB2, 'play/pause_media':0xB3, 'start_mail':0xB4, 'select_media':0xB5, 'start_application_1':0xB6, 'start_application_2':0xB7, 'attn_key':0xF6, 'crsel_key':0xF7, 'exsel_key':0xF8, 'play_key':0xFA, 'zoom_key':0xFB, 'clear_key':0xFE, '+':0xBB, ',':0xBC, '-':0xBD, '.':0xBE, '/':0xBF, '`':0xC0, ';':0xBA, '[':0xDB, '\\':0xDC, ']':0xDD, "'":0xDE, '`':0xC0}
|
||||
VK_CODE = {k.lower():v for k,v in VK_CODE.items()}
|
||||
|
||||
def GrabWindow(hwnd):
|
||||
import win32gui; win32gui.SetForegroundWindow(hwnd); time.sleep(0.3)
|
||||
bbox = tuple(int(v / dpi_scale) for v in win32gui.GetWindowRect(hwnd))
|
||||
return ImageGrab.grab(bbox)
|
||||
|
||||
def imshow(mt, sec=0):
|
||||
cv2.imshow('cc', mt)
|
||||
cv2.waitKey(sec)
|
||||
|
||||
Reference in New Issue
Block a user