Universal Tool

Specially written practical gadgets

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name               Universal Tool
// @name:zh-cn         万能工具
// @namespace          https://github.com/Ocyss/Tampermonkey
// @version            0.3.0
// @description        Specially written practical gadgets
// @description:zh-cn  专门编写实用小工具
// @icon               https://cdn-icons-png.flaticon.com/512/949/949339.png
// @match              *://*/*
// @connect            *
// @grant              CAT_userConfig
// @grant              GM.xmlHttpRequest
// @grant              GM_download
// @grant              GM_getValue
// @grant              GM_openInTab
// @grant              GM_registerMenuCommand
// @grant              GM_setClipboard
// @grant              GM_setValue
// @grant              unsafeWindow
// @grant              window.focus
// @run-at             document-start
// ==/UserScript==

/* ==UserConfig==
{OpenAI: {baseUrl: {title: baseUrl,type: text,default: https://api.openai.com/v1},apiKey: {title: apiKey,type: text},model: {title: model,type: text}}}
  ==/UserConfig== */



(function () {
  'use strict';

  function parseRawHeaders(h) {
    const s = h.trim();
    if (!s) {
      return new Headers();
    }
    const array = s.split("\r\n").map((value) => {
      let s2 = value.split(":");
      return [s2[0].trim(), s2[1].trim()];
    });
    return new Headers(array);
  }
  function parseGMResponse(req, res) {
    const headers = parseRawHeaders(res.responseHeaders);
    const body = typeof res.response === "string" ? new Blob([res.response], { type: headers.get("Content-Type") || "text/plain" }) : res.response;
    return new ResImpl(body, {
      statusCode: res.status,
      statusText: res.statusText,
      headers,
      finalUrl: res.finalUrl,
      redirected: res.finalUrl === req.url
    });
  }
  class ResImpl {
    constructor(body, init) {
      this.rawBody = body;
      this.init = init;
      this.body = body.stream();
      const { headers, statusCode, statusText, finalUrl, redirected } = init;
      this.headers = headers;
      this.status = statusCode;
      this.statusText = statusText;
      this.url = finalUrl;
      this.type = "basic";
      this.redirected = redirected;
      this._bodyUsed = false;
    }
    get bodyUsed() {
      return this._bodyUsed;
    }
    get ok() {
      return this.status < 300;
    }
    arrayBuffer() {
      if (this.bodyUsed) {
        throw new TypeError("Failed to execute 'arrayBuffer' on 'Response': body stream already read");
      }
      this._bodyUsed = true;
      return this.rawBody.arrayBuffer();
    }
    blob() {
      if (this.bodyUsed) {
        throw new TypeError("Failed to execute 'blob' on 'Response': body stream already read");
      }
      this._bodyUsed = true;
      return Promise.resolve(this.rawBody.slice(0, this.rawBody.size, this.rawBody.type));
    }
    clone() {
      if (this.bodyUsed) {
        throw new TypeError("Failed to execute 'clone' on 'Response': body stream already read");
      }
      return new ResImpl(this.rawBody, this.init);
    }
    formData() {
      if (this.bodyUsed) {
        throw new TypeError("Failed to execute 'formData' on 'Response': body stream already read");
      }
      this._bodyUsed = true;
      return this.rawBody.text().then(decode);
    }
    async json() {
      if (this.bodyUsed) {
        throw new TypeError("Failed to execute 'json' on 'Response': body stream already read");
      }
      this._bodyUsed = true;
      return JSON.parse(await this.rawBody.text());
    }
    text() {
      if (this.bodyUsed) {
        throw new TypeError("Failed to execute 'text' on 'Response': body stream already read");
      }
      this._bodyUsed = true;
      return this.rawBody.text();
    }
    async bytes() {
      if (this.bodyUsed) {
        throw new TypeError("Failed to execute 'bytes' on 'Response': body stream already read");
      }
      this._bodyUsed = true;
      return new Uint8Array(await this.rawBody.arrayBuffer());
    }
  }
  function decode(body) {
    const form = new FormData();
    body.trim().split("&").forEach(function(bytes) {
      if (bytes) {
        const split = bytes.split("=");
        const name = split.shift()?.replace(/\+/g, " ");
        const value = split.join("=").replace(/\+/g, " ");
        form.append(decodeURIComponent(name), decodeURIComponent(value));
      }
    });
    return form;
  }
  async function GM_fetch(input, init) {
    const request = new Request(input, init);
    let data;
    if (init?.body) {
      data = await request.text();
    }
    return await XHR(request, init, data);
  }
  function XHR(request, init, data) {
    return new Promise((resolve, reject) => {
      if (request.signal && request.signal.aborted) {
        return reject(new DOMException("Aborted", "AbortError"));
      }
      GM.xmlHttpRequest({
        url: request.url,
        method: gmXHRMethod(request.method.toUpperCase()),
        headers: Object.fromEntries(new Headers(init?.headers).entries()),
        data,
        responseType: "blob",
        onload(res) {
          try {
            resolve(parseGMResponse(request, res));
          } catch (e) {
            reject(e);
          }
        },
        onabort() {
          reject(new DOMException("Aborted", "AbortError"));
        },
        ontimeout() {
          reject(new TypeError("Network request failed, timeout"));
        },
        onerror(err) {
          reject(new TypeError("Failed to fetch: " + err.finalUrl));
        }
      });
    });
  }
  const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "TRACE", "OPTIONS", "CONNECT"];
  function includes(array, element) {
    return array.includes(element);
  }
  function gmXHRMethod(method) {
    if (includes(httpMethods, method)) {
      return method;
    }
    throw new Error(`unsupported http method ${method}`);
  }
  var _GM_download = (() => typeof GM_download != "undefined" ? GM_download : void 0)();
  var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_openInTab = (() => typeof GM_openInTab != "undefined" ? GM_openInTab : void 0)();
  var _GM_registerMenuCommand = (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
  var _GM_setClipboard = (() => typeof GM_setClipboard != "undefined" ? GM_setClipboard : void 0)();
  var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
  async function GM_chat(msg, opt = {}) {
    const baseUrl = _GM_getValue("OpenAI.baseUrl");
    const apiKey = _GM_getValue("OpenAI.apiKey");
    const model = _GM_getValue("OpenAI.model");
    if (!baseUrl || !apiKey || !model) {
      try {
        CAT_userConfig();
      } catch (error) {
        throw Error(`GM_chat没有进行配置, ${error}`);
      }
    }
    const {
      stream = false,
      streamSplit = true,
      console: enableConsole = true
    } = opt;
    const messages = typeof msg === "string" ? [{ role: "user", content: msg }] : msg;
    const requestBody = {
      model,
      messages,
      stream
    };
    try {
      const response = await fetch(`${baseUrl}/chat/completions`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${apiKey}`
        },
        body: JSON.stringify(requestBody)
      });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      if (stream) {
        return await handleStreamResponse(response, {
          streamSplit,
          enableConsole
        });
      } else {
        return await handleNormalResponse(response, { enableConsole });
      }
    } catch (error) {
      console.error("GM_chat 调用失败:", error);
      throw error;
    }
  }
  async function handleNormalResponse(response, { enableConsole }) {
    const data = await response.json();
    const message = data.choices[0].message.content;
    if (enableConsole) {
      console.log("OpenAI Response:", message);
    }
    return {
      message,
      usage: data.usage
    };
  }
  async function handleStreamResponse(response, {
    streamSplit,
    enableConsole
  }) {
    const reader = response.body?.getReader();
    const decoder = new TextDecoder();
    let fullMessage = "";
    let buffer = "";
    if (enableConsole && streamSplit) {
      console.group("OpenAI Stream Response:");
    }
    if (!reader) {
      throw new Error("reader is not defined");
    }
    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop() || "";
        for (const line of lines) {
          if (line.startsWith("data: ")) {
            const data = line.slice(6);
            if (data === "[DONE]") continue;
            try {
              const parsed = JSON.parse(data);
              const delta = parsed.choices[0]?.delta?.content;
              if (delta) {
                fullMessage += delta;
                if (enableConsole) {
                  const parts = delta.split("\n");
                  for (let i = 0; i < parts.length; i++) {
                    if (parts[i] || i < parts.length - 1) {
                      console.log(parts[i]);
                    }
                  }
                }
              }
            } catch (e) {
            }
          }
        }
      }
    } finally {
      reader.releaseLock();
      if (enableConsole && streamSplit) {
        console.groupEnd();
      }
    }
    return { message: fullMessage };
  }
  function createReactiveObject(initialState) {
    const state = { ...initialState };
    return new Proxy(state, {
      get(target, key) {
        return target[key];
      },
      set(target, key, value) {
        target[key] = value;
        return true;
      }
    });
  }
  function createToggleMenu(config) {
    const {
      menuName,
      onStart,
      onStop,
      persistent = true,
      textStart = "🔴/开始",
      textStop = "🟢/停止"
    } = config;
    const state = createReactiveObject({
      ...config.initialState ?? {},
      isRunning: config.defaultRunning ?? false
    });
    const id = `toggle_menu_name_${menuName}`;
    if (persistent) {
      state.isRunning = _GM_getValue(id, state.isRunning);
    }
    function updateMenuCommand() {
      if (state.isRunning) {
        _GM_registerMenuCommand(
          `${textStop}${menuName}`,
          () => {
            state.isRunning = false;
            if (persistent) {
              _GM_setValue(id, false);
            }
            onStop(state);
            updateMenuCommand();
          },
          {
            id
          }
        );
      } else {
        _GM_registerMenuCommand(
          `${textStart}${menuName}`,
          () => {
            state.isRunning = true;
            if (persistent) {
              _GM_setValue(id, true);
            }
            onStart(state);
            updateMenuCommand();
          },
          {
            id
          }
        );
      }
    }
    updateMenuCommand();
    if (state.isRunning) {
      onStart(state);
    }
  }
  const ut = {
    qe(selector, el) {
      return (el ?? document).querySelector(selector);
    },
    qes(selector, el) {
      return (el ?? document).querySelectorAll(selector);
    },
    qesMap(selector, key = "href", el = document) {
      return Array.from(el.querySelectorAll(selector)).map(
        (a) => a[key]
      );
    },
    qx(xpath, el = document) {
      return el.evaluate(xpath, el).iterateNext();
    },
    qxs(xpath, el = document) {
      const results = [];
      const query = el.evaluate(
        xpath,
        el.parentElement || el,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
      );
      for (let i = 0, length = query.snapshotLength; i < length; ++i) {
        results.push(query.snapshotItem(i));
      }
      return results;
    },
    clipboard: _GM_setClipboard,
    setValue: _GM_setValue,
    getValue: _GM_getValue,
    fetch: GM_fetch,
    chat: GM_chat,
    download(items, opt = {}) {
      const defaultLog = (name) => () => console.log(name);
      opt.saveAs ??= true;
      opt.onerror ??= defaultLog("onerror");
      opt.onprogress ??= defaultLog("onprogress");
      opt.ontimeout ??= defaultLog("ontimeout");
      opt.onload ??= defaultLog("onload");
      Array.from(items).forEach((item) => {
        _GM_download({
          ...opt,
          url: item.url,
          name: item.name
        });
      });
    }
  };
  _unsafeWindow.ut = ut;
  _GM_registerMenuCommand("🔍 搜索脚本", () => {
    const host = window.location.host;
    const hostName = Number.isNaN(host.substring(host.lastIndexOf("."))) ? host.substring(
      host.substring(0, host.lastIndexOf(".")).lastIndexOf(".") + 1
    ) : host;
    _GM_openInTab(
      `https://greasyfork.org/zh-CN/scripts/by-site/${hostName}?sort=updated`,
      {
        active: true,
        setParent: true
      }
    );
  });
  createToggleMenu({
    menuName: "平滑滚动",
    persistent: false,
    onStart: (state) => {
      const speed = parseFloat(
        prompt("请输入滚动速度(像素/秒):", "50") ?? "50"
      );
      if (Number.isNaN(speed) || speed <= 0) {
        alert("请输入有效的速度值!");
        return;
      }
      let lastScrollTime = performance.now();
      let scrollAmount = 0;
      function scrollStep(timestamp) {
        if (!state.isRunning) return;
        const deltaTime = timestamp - lastScrollTime;
        lastScrollTime = timestamp;
        scrollAmount += speed * deltaTime / 1e3;
        const scrollDelta = Math.floor(scrollAmount);
        scrollAmount -= scrollDelta;
        window.scrollBy(0, scrollDelta);
        if (state.isRunning) {
          requestAnimationFrame(scrollStep);
        }
      }
      requestAnimationFrame(scrollStep);
    },
    onStop: () => {
      console.log("自动滚动已停止");
    }
  });
  createToggleMenu({
    menuName: "自然滚动",
    persistent: false,
    onStart: (state) => {
      state.scrollTimer = null;
      function randomScroll() {
        if (!state.isRunning) return;
        const scrollDistance = Math.floor(Math.random() * (window.innerHeight - 100)) + window.innerHeight / 3;
        const stayTime = Math.floor(Math.random() * 5e3) + 3e3;
        const startY = window.scrollY;
        const duration = 1e3;
        let startTime = null;
        function scrollStep(timestamp) {
          if (!startTime) startTime = timestamp;
          const progress = timestamp - startTime;
          const scrollDelta = Math.min(progress / duration, 1);
          window.scrollTo(0, startY + scrollDistance * scrollDelta);
          if (progress < duration) {
            requestAnimationFrame(scrollStep);
          } else {
            state.scrollTimer = window.setTimeout(randomScroll, stayTime);
          }
        }
        requestAnimationFrame(scrollStep);
      }
      randomScroll();
    },
    onStop: (state) => {
      if (state.scrollTimer) {
        clearTimeout(state.scrollTimer);
        state.scrollTimer = null;
      }
      console.log("模拟滚动已停止");
    }
  });
  createToggleMenu({
    menuName: "焦点劫持",
    textStart: "🔴/启用",
    textStop: "🟢/禁用",
    defaultRunning: true,
    onStart: (state) => {
      console.log( new Date());
      let allowFocus = false;
      const onMouseDown = () => {
        allowFocus = true;
        setTimeout(() => {
          allowFocus = false;
        }, 100);
      };
      const onBlur = (e) => {
        if (!allowFocus) {
          e.preventDefault();
          e.stopImmediatePropagation();
        }
      };
      document.addEventListener("mousedown", onMouseDown, true);
      const origFocus = window.focus;
      window.focus = function() {
        if (allowFocus) return origFocus.apply(this, arguments);
      };
      window.addEventListener("blur", onBlur, true);
      state.cleanup = () => {
        document.removeEventListener("mousedown", onMouseDown, true);
        window.focus = origFocus;
        window.removeEventListener("blur", onBlur, true);
      };
      console.log("焦点劫持已启用");
    },
    onStop: (state) => {
      state.cleanup?.();
      console.log("焦点劫持已停止");
    }
  });

})();