Greasy Fork

Greasy Fork is available in English.

ChatGPT Exporter

轻松导出 ChatGPT 聊天记录,以便进一步分析或分享。

< 脚本 ChatGPT Exporter 的反馈

评价:一般 - 脚本能用,但还有一些问题

§
发布于:2026-04-30
编辑于:2026-04-30

Great script. OpenAI have recently changed the footer container which has broken the Export button... here's the fixed code, feel free to update the script:

Manual patch diff

--- userscript.js (v2.30.0)
+++ userscript.js (v2.30.1)
@@
-// @version            2.30.0
+// @version            2.30.1

@@
-  const MenuItem = ({ text: text2, successText, disabled = false, title: title2, icon: Icon, onClick, className }) => {
+  const MenuItem = ({ text: text2, successText, disabled = false, title: title2, icon: Icon, onClick, className, variant = "default" }) => {
     const [loading, setLoading] = h$4(false);
     const [succeed, setSucceed] = h$4(false);
     const handleClick = typeof onClick === "function" ? async (e2) => {
       e2.preventDefault();
       if (loading) return;
       try {
         setLoading(true);
         const result = await onClick();
         if (result) {
           setSucceed(true);
           setTimeout(() => setSucceed(false), TIMEOUT);
         }
       } catch (error2) {
         console.error(error2);
       } finally {
         setLoading(false);
       }
     } : void 0;
+    if (variant === "sidebar-footer") {
+      return /* @__PURE__ */ o$8(
+        "div",
+        {
+          className: `group __menu-item hoverable gap-2 ms-2 me-1.5 gap-2! pe-1.5 data-fill:max-w-full [&>div:first-child]:gap-2! ${className ?? ""}`,
+          onClick: handleClick,
+          onTouchStart: handleClick,
+          disabled,
+          title: title2,
+          "data-fill": "",
+          "data-size": "large",
+          "data-sidebar-item": "true",
+          children: loading ? /* @__PURE__ */ o$8("div", { className: "flex justify-center items-center w-full h-full", children: /* @__PURE__ */ o$8(IconLoading, { className: "w-4 h-4" }) }) : /* @__PURE__ */ o$8(k$3, { children: [
+            Icon && /* @__PURE__ */ o$8("div", { className: "flex items-center justify-center [opacity:var(--menu-item-icon-opacity,1)] icon-lg", children: /* @__PURE__ */ o$8(Icon, {}) }),
+            /* @__PURE__ */ o$8("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ o$8("div", { className: "truncate", children: succeed && successText ? successText : text2 }) })
+          ] })
+        }
+      );
+    }
     return /* @__PURE__ */ o$8(
       "div",
       {
         className: `
             menu-item
             flex flex-shrink-0 py-3 px-3 items-center gap-3 rounded-lg mb-2
             bg-menu hover:bg-gray-500/10
             transition-colors duration-200
             text-menu text-sm
             cursor-pointer
             border border-menu ${className}`,

@@
-  function MenuInner({ container }) {
+  function MenuInner({ container, variant = "default" }) {
     const { t: t2 } = useTranslation();
     const disabled = getHistoryDisabled();

@@
     if (disabled) {
       return /* @__PURE__ */ o$8(
         MenuItem,
         {
-          className: "mt-1",
+          className: variant === "sidebar-footer" ? "mb-1" : "mt-1",
           text: "Chat History disabled",
           icon: IconArrowRightFromBracket,
-          disabled: true
+          disabled: true,
+          variant
         }
       );
     }

@@
             /* @__PURE__ */ o$8($cef8881cdc69808e$export$41fb9f06171c75f4, { children: /* @__PURE__ */ o$8(
               MenuItem,
               {
-                className: "mt-1",
+                className: variant === "sidebar-footer" ? "mb-1" : "mt-1",
                 text: t2("ExportHelper"),
                 icon: IconArrowRightFromBracket,
+                variant,
                 onClick: () => {
                   setOpen(true);
                   return true;
                 }
               }
             ) }),

@@
-      /* @__PURE__ */ o$8(Divider, {})
+      variant !== "sidebar-footer" && /* @__PURE__ */ o$8(Divider, {})
     ] });
   }

@@
-  function Menu({ container }) {
-    return /* @__PURE__ */ o$8(SettingProvider, { children: /* @__PURE__ */ o$8(MenuInner, { container }) });
+  function Menu({ container, variant = "default" }) {
+    return /* @__PURE__ */ o$8(SettingProvider, { children: /* @__PURE__ */ o$8(MenuInner, { container, variant }) });
+  }
+  function getSidebarFooterAnchor(nav) {
+    const footer = nav.nextElementSibling;
+    if (!(footer instanceof HTMLElement) || footer.querySelector("nav")) return null;
+    const profileButton = footer.querySelector('[data-testid="accounts-profile-button"]');
+    if (!profileButton) return null;
+    return profileButton.parentElement || footer;
   }

@@
       const styleEl = document.createElement("style");
       styleEl.id = "sentinel-css";
       document.head.append(styleEl);
       const injectionMap = /* @__PURE__ */ new Map();
       const injectNavMenu = (nav) => {
-        if (injectionMap.has(nav)) return;
+        if (injectionMap.has(nav) || nav.hasAttribute("inert") || nav.id === "stage-sidebar-tiny-bar" || nav.className.includes("group/tiny-bar")) return;
         console.log("[Exporter] Injecting nav", nav);
-        const container = getMenuContainer();
+        const sidebarFooter = getSidebarFooterAnchor(nav);
+        const container = getMenuContainer(sidebarFooter ? "sidebar-footer" : "default");
         injectionMap.set(nav, container);
+        if (sidebarFooter) {
+          sidebarFooter.prepend(container);
+          console.log("[Exporter] Prepended container to sidebar footer", sidebarFooter);
+          return;
+        }
         const chatList = nav.querySelector(":scope > div.sticky.bottom-0");
         if (chatList) {
           chatList.prepend(container);
           console.log("[Exporter] Prepended container to chat list", chatList);
         } else {
           container.style.backgroundColor = "#171717";
           container.style.position = "sticky";
           container.style.bottom = "72px";
           nav.append(container);
           console.log("[Exporter] Fallback to appending container to nav", nav);
         }
       };
       sentinel.on("nav", injectNavMenu);
       setInterval(() => {
         injectionMap.forEach((container, nav) => {
-          if (!nav.isConnected) {
+          if (!nav.isConnected || !container.isConnected) {
             container.remove();
             injectionMap.delete(nav);
           }
         });

@@
-  function getMenuContainer() {
+  function getMenuContainer(variant = "default") {
     const container = document.createElement("div");
     container.style.zIndex = "99";
-    D$4(/* @__PURE__ */ o$8(Menu, { container }), container);
+    if (variant === "sidebar-footer") {
+      container.style.width = "100%";
+    }
+    D$4(/* @__PURE__ */ o$8(Menu, { container, variant }), container);
     return container;
   }
pionxzh作者
§
发布于:2026-05-03

Thanks! New version has been published with fix to adapt to footer changes.

发布留言

登录以发布留言。