https://linux.do/t/topic/2209678/75
挂了3h开2c4g成功。
我是用学生邮箱注册账号,按视频教程卡在start simulation。
苦于NVIDIA DSX Air反复登出和start simulation反复失败,让gpt写了个自动点击login和Actions列三个垂直小点并start simulation的油猴脚本。
// ==UserScript==
// @name Web Console Auto Login and Start Task Template
// @namespace web-console-auto-start-template
// @version 1.0
// @description Auto click Login / Actions / Start button, and stop when target status becomes ACTIVE
// @match https://dsx-air.nvidia.com/*
// @match https://air.nvidia.com/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
"use strict";
const config = {
// 请改成你自己页面上创建的Simulation显示的名称
targetName: "YOUR_TARGET_NAME",
// 主循环每 5 秒检测一次
intervalMs: 5000,
// Login 按钮 30 秒最多点一次
loginCooldownMs: 30000,
// Actions 三个点 5 秒最多点一次
actionCooldownMs: 5000,
// Start 按钮 5 秒最多点一次
startCooldownMs: 5000,
// 页面上的按钮 / 菜单 / 状态文本
loginButtonText: "Login",
startButtonText: "Start Simulation",
activeStatusText: "ACTIVE",
inactiveStatusText: "INACTIVE"
};
let lastLoginClick = Number(sessionStorage.getItem("auto_last_login_click") || 0);
let lastActionClick = Number(sessionStorage.getItem("auto_last_action_click") || 0);
let lastStartClick = Number(sessionStorage.getItem("auto_last_start_click") || 0);
let tickCount = 0;
let timer = null;
let stopped = false;
function now() {
return new Date().toLocaleTimeString();
}
function log(...args) {
console.log(`[AUTO ${now()}]`, ...args);
}
function warn(...args) {
console.warn(`[AUTO ${now()}]`, ...args);
}
function getText(el) {
return (el?.innerText || el?.textContent || "").replace(/\s+/g, " ").trim();
}
function isVisible(el) {
if (!el) return false;
const rect = el.getBoundingClientRect();
return (
rect.width > 0 &&
rect.height > 0 &&
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < window.innerHeight &&
rect.left < window.innerWidth
);
}
function fireClick(el, x, y) {
if (!el) return false;
const rect = el.getBoundingClientRect();
const cx = x ?? rect.left + rect.width / 2;
const cy = y ?? rect.top + rect.height / 2;
const eventInit = {
bubbles: true,
cancelable: true,
view: window,
clientX: cx,
clientY: cy,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
button: 0,
buttons: 1
};
const events = [
"pointerover",
"pointerenter",
"mouseover",
"mouseenter",
"pointermove",
"mousemove",
"pointerdown",
"mousedown",
"pointerup",
"mouseup",
"click"
];
for (const type of events) {
const EventClass =
type.startsWith("pointer") && window.PointerEvent
? PointerEvent
: MouseEvent;
el.dispatchEvent(new EventClass(type, eventInit));
}
if (typeof el.click === "function") {
el.click();
}
return true;
}
function deepClick(el) {
if (!el) return false;
const rect = el.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
log("准备 deepClick:", {
tag: el.tagName,
x: Math.round(x),
y: Math.round(y),
html: el.outerHTML?.slice(0, 300)
});
// 1. 点击目标元素本身
fireClick(el, x, y);
// 2. 再沿父元素往上点几层,适配事件绑定在父层的情况
let p = el.parentElement;
for (let i = 0; i < 5 && p; i++) {
fireClick(p, x, y);
p = p.parentElement;
}
// 3. 再点击当前位置最上层元素
const stack = document.elementsFromPoint(x, y);
if (stack && stack.length > 0) {
fireClick(stack[0], x, y);
}
return true;
}
function isLoginPage() {
const bodyText = document.body?.innerText || "";
return (
location.pathname.toLowerCase().includes("/login") ||
bodyText.includes("sign in") ||
bodyText.includes("Sign in") ||
bodyText.includes("please sign in")
);
}
function findLoginButton() {
const candidates = Array.from(
document.querySelectorAll("button, [role='button'], a, div, span")
).filter((el) => {
return isVisible(el) && getText(el).toLowerCase() === config.loginButtonText.toLowerCase();
});
if (!candidates.length) return null;
const scored = candidates.map((el) => {
const rect = el.getBoundingClientRect();
const area = rect.width * rect.height;
const cls = el.className?.toString() || "";
let score = 0;
if (el.tagName.toLowerCase() === "button") score += 100;
if (el.getAttribute("role") === "button") score += 80;
// 常见按钮 class 加分
if (cls.toLowerCase().includes("button")) score += 80;
if (cls.toLowerCase().includes("primary")) score += 80;
if (cls.toLowerCase().includes("brand")) score += 60;
// 优先页面中间区域的 Login,避免误点顶部导航栏
if (rect.top > 180) score += 200;
if (rect.top < 150) score -= 300;
if (
rect.left > window.innerWidth * 0.15 &&
rect.right < window.innerWidth * 0.85
) {
score += 120;
}
score += Math.min(area / 100, 150);
return { el, score };
});
scored.sort((a, b) => b.score - a.score);
return scored[0]?.el || null;
}
function clickLoginIfNeeded() {
const current = Date.now();
if (current - lastLoginClick < config.loginCooldownMs) {
const left = Math.ceil(
(config.loginCooldownMs - (current - lastLoginClick)) / 1000
);
warn(`当前疑似 Login 页面,但刚点过 Login,还需等待 ${left} 秒。`);
return;
}
const loginButton = findLoginButton();
if (!loginButton) {
warn("没有找到 Login 按钮。");
return;
}
lastLoginClick = current;
sessionStorage.setItem("auto_last_login_click", String(current));
log("点击 Login:", loginButton);
deepClick(loginButton);
}
function findExactTextElement(text) {
const elements = Array.from(
document.querySelectorAll("a, button, span, div, td, th")
);
return elements.find((el) => {
return isVisible(el) && getText(el) === text;
});
}
function findTargetNameElement() {
return findExactTextElement(config.targetName);
}
function findTargetStatusElement() {
const nameEl = findTargetNameElement();
if (!nameEl) {
return null;
}
const nameRect = nameEl.getBoundingClientRect();
// 1. 优先在目标名称所在的父级容器里找 ACTIVE / INACTIVE
let parent = nameEl;
for (let i = 0; i < 10 && parent; i++) {
const statusCandidates = Array.from(
parent.querySelectorAll("span, div, td, th, button, [role='cell']")
).filter((el) => {
const text = getText(el).toUpperCase();
return (
isVisible(el) &&
(text === config.activeStatusText || text === config.inactiveStatusText)
);
});
if (statusCandidates.length > 0) {
statusCandidates.sort((a, b) => {
const ar = a.getBoundingClientRect();
const br = b.getBoundingClientRect();
const ayDiff = Math.abs(
ar.top + ar.height / 2 - (nameRect.top + nameRect.height / 2)
);
const byDiff = Math.abs(
br.top + br.height / 2 - (nameRect.top + nameRect.height / 2)
);
return ayDiff - byDiff;
});
return statusCandidates[0];
}
parent = parent.parentElement;
}
// 2. 如果父级里没找到,就全局找和目标名称在同一行附近的状态
const allStatusCandidates = Array.from(
document.querySelectorAll("span, div, td, th, button, [role='cell']")
).filter((el) => {
const text = getText(el).toUpperCase();
if (!isVisible(el)) return false;
if (text !== config.activeStatusText && text !== config.inactiveStatusText) {
return false;
}
const rect = el.getBoundingClientRect();
const centerY = rect.top + rect.height / 2;
const nameCenterY = nameRect.top + nameRect.height / 2;
const yDiff = Math.abs(centerY - nameCenterY);
// 状态一般在目标名称右侧,并且和名称在同一行
return yDiff < 50 && rect.left > nameRect.left;
});
if (!allStatusCandidates.length) {
return null;
}
allStatusCandidates.sort((a, b) => {
const ar = a.getBoundingClientRect();
const br = b.getBoundingClientRect();
const ayDiff = Math.abs(
ar.top + ar.height / 2 - (nameRect.top + nameRect.height / 2)
);
const byDiff = Math.abs(
br.top + br.height / 2 - (nameRect.top + nameRect.height / 2)
);
return ayDiff - byDiff;
});
return allStatusCandidates[0];
}
function getTargetStatusText() {
const statusEl = findTargetStatusElement();
if (!statusEl) {
return "";
}
return getText(statusEl).toUpperCase();
}
function isTargetActive() {
const status = getTargetStatusText();
// 注意:这里必须精确等于 ACTIVE
// 不能用 includes("ACTIVE")
// 因为 INACTIVE 里面也包含 ACTIVE
return status === config.activeStatusText;
}
function stopWhenTargetActive() {
const status = getTargetStatusText();
if (status) {
log(`当前目标状态:${status}`);
}
if (!isTargetActive()) {
return false;
}
log(`检测到 ${config.targetName} 已经变成 ${config.activeStatusText},准备停止脚本。`);
const alertKey = `auto_active_alerted_${config.targetName}`;
if (sessionStorage.getItem(alertKey) !== "1") {
sessionStorage.setItem(alertKey, "1");
alert(`目标 ${config.targetName} 已经变成 ${config.activeStatusText},脚本已自动停止。`);
}
stop();
return true;
}
function findStartItem() {
const candidates = Array.from(
document.querySelectorAll(
"button, [role='button'], [role='menuitem'], a, div, span"
)
).filter((el) => {
return (
isVisible(el) &&
getText(el).toLowerCase() === config.startButtonText.toLowerCase()
);
});
if (!candidates.length) return null;
candidates.sort((a, b) => {
const ar = a.getBoundingClientRect();
const br = b.getBoundingClientRect();
let as = ar.width * ar.height;
let bs = br.width * br.height;
if (a.getAttribute("role") === "menuitem") as += 10000;
if (b.getAttribute("role") === "menuitem") bs += 10000;
return bs - as;
});
return candidates[0];
}
function clickStartIfVisible() {
const current = Date.now();
if (current - lastStartClick < config.startCooldownMs) {
return false;
}
const item = findStartItem();
if (!item) {
return false;
}
lastStartClick = current;
sessionStorage.setItem("auto_last_start_click", String(current));
log(`点击 ${config.startButtonText}:`, item);
deepClick(item);
return true;
}
function findActionThreeDotElement() {
const nameEl = findTargetNameElement();
if (!nameEl) {
warn(`没有找到目标名称:${config.targetName}`);
return null;
}
const nameRect = nameEl.getBoundingClientRect();
const rowY = nameRect.top + nameRect.height / 2;
// 常见“三个点 / 更多操作”图标
const candidates = Array.from(
document.querySelectorAll(
[
'svg[data-src*="more"]',
'svg[data-src*="more-vert"]',
'svg[data-id*="svg-loader"]',
'use[href*="more"]',
'use[href*="more-vert"]',
'[aria-label*="More"]',
'[aria-label*="more"]',
'[title*="More"]',
'[title*="more"]',
'button',
'[role="button"]'
].join(", ")
)
).filter(isVisible);
const scored = candidates
.map((el) => {
const rect = el.getBoundingClientRect();
const centerY = rect.top + rect.height / 2;
let score = 0;
const yDiff = Math.abs(centerY - rowY);
// 必须尽量和目标名称在同一行
score -= yDiff * 20;
// 通常 Actions / More 按钮在右侧
score += rect.left;
const html = el.outerHTML || "";
const text = getText(el);
if (html.toLowerCase().includes("more")) score += 2000;
if (html.toLowerCase().includes("more-vert")) score += 2000;
if (html.toLowerCase().includes("svg-loader")) score += 500;
if (text === "⋮" || text === "...") score += 1500;
// 排除顶部导航栏图标
if (rect.top < 150) score -= 1000;
return { el, rect, centerY, yDiff, score };
})
.filter((item) => {
return item.yDiff < 50 && item.rect.left > window.innerWidth * 0.5;
});
scored.sort((a, b) => b.score - a.score);
log(
"Actions / More 候选:",
scored.map((c) => ({
score: Math.round(c.score),
yDiff: Math.round(c.yDiff),
left: Math.round(c.rect.left),
top: Math.round(c.rect.top),
width: Math.round(c.rect.width),
height: Math.round(c.rect.height),
html: c.el.outerHTML?.slice(0, 160)
}))
);
return scored[0]?.el || null;
}
function clickActionThreeDots() {
const current = Date.now();
if (current - lastActionClick < config.actionCooldownMs) {
return false;
}
const actionEl = findActionThreeDotElement();
if (!actionEl) {
warn("没有找到 Actions / More 三个点按钮。");
return false;
}
lastActionClick = current;
sessionStorage.setItem("auto_last_action_click", String(current));
log("点击 Actions / More:", actionEl);
deepClick(actionEl);
return true;
}
function tick() {
if (stopped) return;
tickCount += 1;
log(`第 ${tickCount} 次检测:${location.href}`);
if (isLoginPage()) {
clickLoginIfNeeded();
return;
}
// 每次点击之前,先检查是否已经 ACTIVE
// 一旦 ACTIVE,弹窗提醒并停止脚本
if (stopWhenTargetActive()) {
return;
}
// 如果菜单已经展开,优先点 Start
if (clickStartIfVisible()) {
return;
}
// 再检查一次,防止刚点完 Start 后页面立即变 ACTIVE
if (stopWhenTargetActive()) {
return;
}
// 否则先点 Actions / More 三个点
const clickedActions = clickActionThreeDots();
if (clickedActions) {
setTimeout(() => {
if (stopped) return;
// 点 Start 前再检查一次 ACTIVE
if (stopWhenTargetActive()) {
return;
}
clickStartIfVisible();
// 点完 Start 后再延迟检查一次
setTimeout(() => {
if (stopped) return;
stopWhenTargetActive();
}, 1500);
}, 1000);
}
}
function stop() {
stopped = true;
if (timer) {
clearInterval(timer);
}
console.log("[AUTO] 已停止。");
}
// 暴露到 window,方便在控制台手动停止或手动触发一次检测
window.__autoStartStop = stop;
window.__autoStartTick = tick;
log(
"自动化脚本模板已加载。Login 每 30 秒一次,Actions / Start 每 5 秒一次;检测到 ACTIVE 后自动停止。"
);
setTimeout(tick, 1500);
timer = setInterval(tick, config.intervalMs);
})();
在油猴里自建脚本粘贴代码,把targetName改成自己的simulation名称并打开 NVIDIA DSX Air 即可食用。
2 个帖子 - 2 位参与者