sora 2 advanced bio editor

format your bio :yay:

当前为 2025-10-13 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         sora 2 advanced bio editor
// @namespace    http://alexbadi.es
// @version      1.0.0
// @description  format your bio :yay:
// @author       Alex Badi
// @match        https://sora.chatgpt.com/profile
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com
// @grant        none
// @license      GPL-3.0
// ==/UserScript==

(async function waitForContainer() {
  const container = document.querySelector(
    "section.relative.flex.w-full.flex-col.items-center.text-center > div.flex.w-full.flex-wrap.items-center.justify-center.gap-3"
  );

  if (!container) return setTimeout(waitForContainer, 500);
  if (container.childElementCount < 2) return setTimeout(waitForContainer, 500);
  if (container.querySelector(".advanced-bio-btn")) return;

  let __wp;
  window.webpackChunk_N_E.push([
    [Math.random()],
    {},
    (r) => {
      __wp = r;
    },
  ]);

  const wp98121 = __wp(98121);
  const wp92964 = __wp(92964);
  const toast = wp98121?.Am;

  let network = null;
  let authHeaders = null;

  if (__wp(72038)?.ZP) {
    network = __wp(72038).ZP;
    try {
      authHeaders = await network.getAuthHeaders?.();
    } catch (e) {
      console.warn("getAuthHeaders failed, will fallback to accessToken");
      toast.warn("Couldn't get Auth Headers, will fallback to Session");
    }
  } else {
    console.warn("__wp(72038).ZP not found, will fallback to accessToken");
    toast.warn(
      "wp(72038).ZP not found, will fallback to Session. Sora probably updated."
    );
  }

  let authToken = "";

  async function getAccessToken() {
    if (!wp92964?.hP) return null;
    const session = await wp92964.hP();
    if (!session?.accessToken || typeof session.accessToken !== "string")
      return null;
    return session.accessToken;
  }

  const originalFetch = window.fetch;
  async function ensureAuthToken() {
    if (authHeaders) return;
    if (!authToken) {
      const token = await getAccessToken();
      if (!token) return;
      authToken = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
    }
  }

  window.fetch = async function (url, options = {}) {
    await ensureAuthToken();
    if (!authHeaders) {
      toast.warn(
        "Session failed, fallback to Fetch modification. Please update your bio first."
      );
      if (!options.headers) options.headers = {};
      if (!options.headers.Authorization && authToken) {
        options.headers.Authorization = authToken;
      }
    }
    return originalFetch(url, options);
  };

  const button = document.createElement("button");
  button.type = "button";
  button.className =
    "advanced-bio-btn inline-flex gap-1.5 items-center justify-center whitespace-nowrap text-sm font-semibold focus-visible:outline-none data-[disabled=true]:pointer-events-none data-[disabled=true]:cursor-default data-[disabled=true]:opacity-50 group/button relative shadow-inset-stroke text-token-text-primary hover:bg-token-bg-active data-[state=open]:bg-token-bg-active hover:shadow-none data-[state=open]:shadow-none py-2 rounded-full h-11 flex-1 px-12";
  button.textContent = "Advanced bio editor";

  button.addEventListener("click", async () => {
    if (!authHeaders && !authToken) {
      const token = await getAccessToken();
      if (!token) {
        toast?.warning("Please edit your current bio to continue.");
        return;
      }
      authToken = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
    }

    const main = document.querySelector("main");
    if (!main) return;

    const overlay = document.createElement("div");
    overlay.setAttribute("data-state", "open");
    overlay.className = "fixed inset-0 z-dialog bg-black/80";
    overlay.style.pointerEvents = "auto";

    const dialog = document.createElement("div");
    dialog.setAttribute("role", "dialog");
    dialog.setAttribute("data-state", "open");
    dialog.className =
      "dialog-content surface-popover fixed left-1/2 top-1/2 z-dialog grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] rounded-2xl p-6 shadow-2xl bg-token-bg-primary py-12";
    dialog.tabIndex = -1;

    dialog.innerHTML = `
        <div class="relative">
            <button type="button" id="closeModalBtn" class="fixed left-3 top-2 z-dialog h-8 w-8 flex items-center justify-center rounded-full bg-tertiary p-1 opacity-70 hover:opacity-100">✕</button>
            <div class="max-h-[calc(100svh-4rem)] overflow-y-auto">
                <div class="mx-auto flex w-full max-w-md flex-col items-center gap-4">
                <div class="flex flex-col items-center text-center gap-1">
                    <h2 class="font-semibold text-2xl">Edit your bio</h2>
                    <p class="text-sm max-w-64">You can use new lines and add ASCII images.</p>
                </div>
                <div class="w-full">
                    <div class="pl-4 text-xs text-token-text-secondary">Your biography</div>
                    <textarea id="bioTextarea" class="mt-2 w-full max-w-[42ch] rounded-xl bg-token-bg-lighter" style="padding: 12px; height: 250px; font-family: monospace; font-size: 16px; white-space: pre-wrap; overflow-wrap: break-word;"></textarea>
                    <button type="button" id="centerSelectionButton" class="h-8 w-8 flex items-center justify-centerp-1 opacity-70 hover:opacity-100" style="display: none;">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="none">
                        <path fill-rule="evenodd" clip-rule="evenodd" d="M5 7C5 6.44772 5.44772 6 6 6H18C18.5523 6 19 6.44772 19 7C19 7.55228 18.5523 8 18 8H6C5.44772 8 5 7.55228 5 7ZM5 12C5 11.4477 5.44772 11 6 11H18C18.5523 11 19 11.4477 19 12C19 12.5523 18.5523 13 18 13H6C5.44772 13 5 12.5523 5 12ZM8 17C8 16.4477 8.44772 16 9 16H15C15.5523 16 16 16.4477 16 17C16 17.5523 15.5523 18 15 18H9C8.44772 18 8 17.5523 8 17Z" fill="#fff"></path>
                    </svg>
                    </button>
                </div>
                <div class="flex flex-col gap-2 py-2 tablet:flex-row tablet:gap-6 rounded-xl bg-token-bg-lighter" style="padding: 25px;margin-bottom: 10px;">
                    <div class="flex shrink-0 flex-col gap-2 tablet:w-3/5" style="width: 80% !important;">
                    <div class="font-semibold">Reload after editing</div>
                    <div class="leading-[18px] text-token-text-secondary">
                        <div>It is recommended to reload the page after editing.</div>
                    </div>
                    </div>
                    <div>
                    <button type="button" role="switch" aria-checked="false" data-state="unchecked" value="off" class="peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 h-6 w-11 focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-secondary" id="rswitch">
                        <span data-state="unchecked" class="pointer-events-none block rounded-full shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 data-[state=checked]:translate-x-5 h-5 w-5 bg-background"></span>
                    </button>
                    </div>
                </div>
                <div class="flex w-full gap-4">
                    <button id="doneBtn" class="inline-flex gap-1.5 items-center justify-center text-sm font-semibold bg-token-bg-inverse text-token-text-inverse px-3 py-2 rounded-full h-12 flex-1 disabled:opacity-50">
                    <div style="height: 16px;width: 16px;">
                        <div class="spin_loader"></div>
                    </div> Done
                    </button>
                </div>
            </div>
        </div>
        `;

    const style = document.createElement("style");
    style.id = "spin-loader-style";
    style.textContent = `
        @keyframes spinrotation { 100% { transform: rotate(360deg); } }
                    .spin_loader {
                        animation: spinrotation 1s linear infinite;
                        aspect-ratio: 1/1;
                        border: .2rem solid #d9d9d9;
                        border-bottom-color: #2e2e2e;
                        border-radius: 50%;
                        box-sizing: border-box;
                        display: inline-block;
                        height: 100%;
                    }

        button:disabled .spin_loader {
        display: inline-block;
        }

        button:not(:disabled) .spin_loader {
        display: none;
        }
  `;

    document.head.appendChild(style);

    main.appendChild(overlay);
    main.appendChild(dialog);

    const textarea = document.getElementById("bioTextarea");
    const centerBtn = document.getElementById("centerSelectionButton");

    centerBtn.addEventListener("click", () => {
      const start = textarea.selectionStart;
      const end = textarea.selectionEnd;
      if (start === end) return;

      const selectedText = textarea.value.slice(start, end);

      const maxChars = 42;

      const lines = selectedText.split("\n");

      const centeredLines = lines.map((line) => {
        const trimmed = line.trim();
        const spacesToAdd = Math.floor((maxChars - trimmed.length) / 2);
        const padding = " ".repeat(Math.max(0, spacesToAdd));
        return padding + trimmed;
      });

      const newText = centeredLines.join("\n");

      textarea.setRangeText(newText, start, end, "end");

      textarea.selectionStart = start;
      textarea.selectionEnd = start + newText.length;
      textarea.focus();
    });

    let ischecked = false;
    const switchEl = dialog.querySelector("#rswitch");
    const switchHandle = dialog.querySelector("#rswitch span");

    function setChecked(value) {
      switchEl.setAttribute("aria-checked", value.toString());
      switchEl.setAttribute("data-state", value ? "checked" : "unchecked");
      switchEl.setAttribute("value", (value && "on") || "off");
      switchHandle.setAttribute("data-state", value ? "checked" : "unchecked");

      ischecked = value;
    }

    dialog.querySelector("#rswitch").onclick = function () {
      setChecked(!ischecked);
    };

    dialog.querySelector("#closeModalBtn").addEventListener("click", () => {
      overlay.remove();
      dialog.remove();
    });

    const originalBioElement = document.querySelector(
      "body > main > div.h-full.max-h-screen.min-h-screen.w-full > div.h-full.max-h-screen.min-h-screen.w-full > div > div > div > div > div > section.relative.flex.w-full.flex-col.items-center.gap-4.text-center.tablet\\:max-w-md > div.flex.w-full.flex-col.items-center.gap-2 > div:nth-child(2) > button"
    );
    const secondaryBioElement = document.querySelector(
      "body > main > div.h-full.max-h-screen.min-h-screen.w-full > div.h-full.max-h-screen.min-h-screen.w-full > div > div > div > div > div > section.relative.flex.w-full.flex-col.items-center.gap-4.text-center.tablet\\:max-w-md > div.flex.w-full.flex-col.items-center.gap-2 > div:nth-child(2)"
    );
    const currentBio =
      originalBioElement?.innerText || secondaryBioElement?.innerText || "";
    dialog.querySelector("#bioTextarea").value = currentBio;

    async function done(doReload) {
      dialog.querySelector("#doneBtn").disabled = true;
      const bioText = dialog.querySelector("#bioTextarea").value;

      let userId = null;
      let deviceId = null;

      if (__STATSIG_SDK__) {
        const currentUser = __STATSIG_SDK__.getCurrentUser();
        userId = currentUser.userID;
        deviceId = currentUser.customIDs.stableID;
      } else {
        for (let i = 0; i < localStorage.length; i++) {
          const key = localStorage.key(i);
          if (key.includes("user")) {
            userId = key.split("/")[1];
            break;
          }
        }
        const store = JSON.parse(
          localStorage.getItem("STATSIG_LOCAL_STORAGE_INTERNAL_STORE_V4") ||
            "{}"
        );
        const firstKey = Object.keys(store)[0];
        deviceId = store[firstKey]?.stableIDUsed;
      }

      if (!userId || !deviceId) {
        console.error("Cannot find userId or deviceId");
        toast.error("Cannot find userId or deviceId");
        dialog.querySelector("#doneBtn").disabled = false;
        return;
      }

      try {
        const response = await fetch(
          `https://sora.chatgpt.com/backend/project_y/profile/${userId}`,
          {
            method: "POST",
            headers: {
              accept: "*/*",
              "content-type": "application/json",
              "oai-device-id": deviceId,
              Authorization: authHeaders?.Authorization || authToken,
            },
            body: JSON.stringify({ description: bioText }),
            credentials: "include",
          }
        );
        if (!response.ok) {
          toast.error(`Response wasn't ok. Status: ${response.status}`);
        } else {
          toast.success(`Your bio was edited! Congrats!`);
          if (doReload) {
            setTimeout(() => {
              toast.success(`Reloading`);
              window.location.reload();
            }, 1000);
          }
        }
        if (originalBioElement) {
          originalBioElement.innerText = bioText;
          originalBioElement.outerText = bioText;
        }
        console.log("Bio updated successfully!");
      } catch (err) {
        console.error("Failed to update bio:", err);
      }

      overlay.remove();
      dialog.remove();
      style.remove();
    }

    dialog.querySelector("#doneBtn").addEventListener("click", async () => {
      await done(ischecked);
    });
  });

  container.appendChild(button);
})();