分享自动登录NVIDIA DSX Air,start simulation的油猴脚本

https://linux.do/t/topic/2209678/75 挂了3h开2c4g成功。 我是用学生邮箱注册账号,按视频教程卡在start simulation。 苦于NVIDIA DSX Air反复登出和start simulation反复失败,让gpt写了个自动点击login和Actio...
分享自动登录NVIDIA DSX Air,start simulation的油猴脚本
分享自动登录NVIDIA DSX Air,start simulation的油猴脚本

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 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文