ST QR Collection Importer

SillyTavern QuickReply preset collection importer.

// ==UserScript==
// @name        ST QR Collection Importer
// @namespace   Violentmonkey Scripts
// @match       http://127.0.0.1:8000/*
// @grant       none
// @version     1.0
// @author      creamy
// @license     MIT
// @description SillyTavern QuickReply preset collection importer.
// ==/UserScript==
(function() {
  "use strict";


  window.addEventListener("load", init);


  function init() {
    //main cont
    const main = document.createElement("div");
    main.setAttribute("id", "main");
    main.setAttribute("class", "popup");
    Object.assign(main.style, {
      width: "auto",
      height: "auto",
      margin: "auto",
      padding: "5px",
      position: "absolute",
      top: "5px",
      right: "5px",
      zIndex: "99999",
      opacity: "70%"
    })

    const fALink = document.createElement("link");
    fALink.setAttribute("rel", "stylesheet");
    fALink.setAttribute("href", "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css");
    main.appendChild(fALink);

    //logo button
    const logo = document.createElement("div");
    logo.innerHTML = '<i class="fa-solid fa-file-import"></i>';
    logo.setAttribute("id", "showbtn");
    logo.setAttribute("class", "menu_button");
    Object.assign(logo.style, {
      fontSize: "1.5em",
      float: "right"
    })

    //Update presets title
    const updateLabel = document.createElement("div");
    updateLabel.innerText = "Import QR preset collection";
    Object.assign(updateLabel.style, {
      textAlign: "center",
      fontSize: "1.2em",
      width: "200px",
      display: "none"
    })

    //logo title container
    const logoTitleCont = document.createElement("div");
    logoTitleCont.appendChild(updateLabel);
    logoTitleCont.appendChild(logo);
    Object.assign(logoTitleCont.style, {
      padding: "2px",
      display: "flex",
      gap: "5px",
      alignItems: "center"
    })

    //input
    const inputCol = document.createElement("input");
    inputCol.setAttribute("id", "inputCol");
    inputCol.setAttribute("name", "inputCol");
    inputCol.setAttribute("type", "text");
    inputCol.setAttribute("class", "text-pole")
    Object.assign(inputCol.style, {
      display: "none",
      backgroundColor: "#3a7a41"
    })

    //button
    const button = document.createElement("button");
    button.setAttribute("id", "updatebtn");
    button.setAttribute("type", "button");
    button.setAttribute("class", "menu_button");
    button.innerText = "Import";
    Object.assign(button.style, {
      display: "none",
    })

    //append to document
    main.appendChild(logoTitleCont);
    main.appendChild(inputCol);
    main.appendChild(button);
    document.querySelector("body").appendChild(main);

    logo.addEventListener("click", function() {
      if(updateLabel.style.display === "none") {
        updateLabel.style.display = "block";
        inputCol.style.display = "block";
        button.style.display = "flex";
        logo.innerHTML = '<i class="fa-solid fa-circle-xmark"></i>';
      }
      else {
        updateLabel.style.display = "none";
        inputCol.style.display = "none";
        button.style.display = "none";
        logo.innerHTML = '<i class="fa-solid fa-file-import"></i>';
      }
    });

    button.addEventListener("click", async function handleImportBtnClick() {
      toastr.info("Importing... Please wait.");
      const presets = await getPresets(inputCol.value)
      importQRS(presets.map((preset) => preset.link))
    });
  }


  const noCacheParam = `${Date.now()}_${Math.floor(Math.random() * 1000000)}`;
  async function getPresets(inputLink) {
    return new Promise(async (resolve, reject) => {
      const corsProxyUrl = `https://corsproxy.io/?url=${inputLink}?nocache=${noCacheParam}`
      const response = await fetch(corsProxyUrl)
      if(response.status === 200) {
        try {
          const presetsFromLink = await response.json()
          resolve([...presetsFromLink])
        }
        catch {
          toastr.error("Import failed.")
        }
      }
      else {
        console.error(`Error with the link: ${inputLink}`)
        toastr.error(`Error with the link: ${inputLink}`)
      }
    })
  }


  //Credits: rentry.org/stscript
  async function importQRS(QR_JSON_URLS) {
    //put your json urls here
    //const QR_JSON_URLS = [];

    /**
     *  DO NOT FUCK AROUND WITH THE STUFF BELOW
     *  UNLESS YOU KNOW WHAT YOU ARE DOING
     */


    /**
     * Loads SillyTavern QuickReply API instance
     *
     * @returns {Promise<QuickReplyApi>}
     */
    const loadQrApi = async () => {
        const { quickReplyApi } = await import('./scripts/extensions/quick-reply/index.js');
        return quickReplyApi;
    }

    /**
     * Fetches JSON object from a given URL
     *
     * @param {string} url - The URL to get the JSON from
     *
     * @returns {Promise<object>} - The parsed Object
     */
    const fetchJson = async (url) => {
        const corsProxyUrl = "https://corsproxy.io/?url=";
        const response = await fetch(`${corsProxyUrl}${url}?nocache=${noCacheParam}`);

        return await response.json();
    }

    /**
     * Update a QuickReply withing a QuickReplySet;
     * This will overwrite the set
     *
     * @param {QuickReplyApi} api - The ST QR API instance
     * @param {QuickReplySet} set - The QuickReplySet in which to update the QuickReply
     * @param {QuickReply} qr - The QuickReply data to update with
     *
     * @returns {Promise<void>}
     */
    const updateQuickReply = async (api, set, qr) => {
        console.log("Updating existing qr", qr, "in set", set);
        api.updateQuickReply(set.name, qr.label, { ...qr });
    };

    /**
     * Create a QuickReply within a QuickReplySet
     *
     * @param {QuickReplyApi} api - The ST QR API instance
     * @param {QuickReplySet} set - The QuickReplySet in which to create the QuickReply
     * @param {QuickReply} qr - The QuickReply to create
     *
     * @returns {Promise<void>}
     */
    const createQuickReply = (api, set, qr) => {
        console.log("Creating new qr", qr, "in set", set);
        api.createQuickReply(set.name, qr.label, { ...qr });
    };

    /**
     * Update an already existing QuickReplySet;
     * This will overwrite the qr
     *
     * @param {QuickReplyApi} api - The ST QR API instance
     * @param {QuickReplySet} set - The already existing QuickReplySet
     * @param {object} data - The data to update the set with
     *
     * @returns {Promise<void>}
     */
    const updateQuickReplySet = async (api, set, data) => {
        console.log("Updating set", set, data);

        await api.updateSet(set.name, { ...data });

        for (const qr of data.qrList) {
            const existingQr = await api.getQrByLabel(set.name, qr.label);

            if (existingQr) {
                await updateQuickReply(api, set, qr);
            } else {
                await createQuickReply(api, set, qr);
            }
        }
    }

    /**
     * Create a new QuickReplySet
     *
     * @param {QuickReplyApi} api - The ST QR API instance
     * @param {object} data - The data to create the set with
     *
     * @returns {Promise<void>}
     */
    const createQuickReplySet = async (api, data) => {
        console.log("Creating new set", data);

        const set = await api.createSet(data.name);
        return updateQuickReplySet(api, set, data);
    }

    // Main
    const api = await loadQrApi();

    for (const url of QR_JSON_URLS) {
        console.log("Loading", url);

        try {
            const data = await fetchJson(url);
            const set = await api.getSetByName(data.name);

            if (set) {
                await updateQuickReplySet(api, set, data);
            } else {
                await createQuickReplySet(api, data);
            }
        } catch (e) {
            console.error("Failed to load", url, e);
            toastr.error("Failed to load: " + url + " " + e);
        }
    }

    toastr.success("QR import success. Reload the page.");
  }


})()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址