You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firefox扩展添加ChatGPT文件上传按钮仅单页生效的问题排查

问题分析

ChatGPT是单页应用(SPA),切换聊天页面时不会刷新整个页面,仅通过History API更新URL。你的现有逻辑只在页面初始加载或刷新时执行content script,切换页面时不会重新运行,导致按钮不显示。另外manifest.json的background配置存在错误,Manifest V3规范不允许同时指定scripts和service_worker字段。

解决方案

1. 修复manifest.json配置

移除background字段中的"scripts": ["background.js"],同时调整权限配置以符合V3规范:

{
    "manifest_version": 3,
    "name": "ChatGPT File Upload",
    "version": "1.0",
    "description": "为ChatGPT添加文件上传按钮。(不支持图片、视频、Word文档等非纯文本文件,请使用.txt、.js、.py、.html、.css、.json和.csv格式)",
    "permissions": [
      "scripting",
      "activeTab"
    ],
    "host_permissions": [
      "https://chat.openai.com/*"
    ],
    "action": {
      "default_icon": {
        "128": "icon128.png",
        "256": "icon128.png"
      }
    },
    "icons": {
      "128": "icon128.png",
      "256": "icon256.png"
    },
    "content_scripts": [
      {
        "matches": ["https://chat.openai.com/*"],
        "js": ["content.js"]
      }
    ],
    "background": {
      "service_worker": "background.js"
    }
  }  

注:将原权限中的URL移至host_permissions是Manifest V3的强制规范;添加activeTab权限可提升扩展安全性。

2. 简化background.js

由于content script已通过matches字段自动注入,无需再通过tabs.onUpdated重复注入,可将background.js简化(或直接删除):

// 无需重复注入content script,此文件可留空或删除

3. 重构content.js适配SPA页面切换

核心优化点:

  • 封装按钮创建逻辑,避免重复生成
  • 监听SPA的URL变化(覆盖History API和浏览器前进后退)
  • 用MutationObserver等待目标元素加载,解决页面渲染时序问题

修改后的content.js:

console.log("Content script loaded.");

// 封装创建上传按钮的函数
function createUploadButton() {
  // 检查按钮是否已存在,避免重复创建
  if (document.querySelector('#chatgpt-file-upload-btn')) {
    return;
  }

  // 创建按钮
  const button = document.createElement('button');
  button.id = 'chatgpt-file-upload-btn';
  button.innerText = '📂 提交文件';
  button.style.backgroundColor = '#35393d';
  button.style.color = 'white';
  button.style.padding = '5px';
  button.style.border = '1px solid #6b6458';
  button.style.borderRadius = '5px';
  button.style.margin = '5px';
  button.style.width = '180px';

  // 创建居中容器
  const containerDiv = document.createElement('div');
  containerDiv.style.display = 'flex';
  containerDiv.style.justifyContent = 'center';
  containerDiv.appendChild(button);

  // 目标元素选择器
  const targetSelector = "div.relative.flex.h-full.max-w-full.flex-1.overflow-hidden > div > main > div.absolute.bottom-0 > form > div > div:nth-child(1)";
  let targetElement = document.querySelector(targetSelector);

  if (targetElement) {
    // 直接插入按钮
    targetElement.parentNode.insertBefore(containerDiv, targetElement);
  } else {
    // 用MutationObserver等待目标元素加载完成
    const observer = new MutationObserver((mutations) => {
      targetElement = document.querySelector(targetSelector);
      if (targetElement) {
        targetElement.parentNode.insertBefore(containerDiv, targetElement);
        observer.disconnect();
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  // 按钮点击事件逻辑
  button.addEventListener('click', async () => {
    const fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.accept = '.txt, .js, .py, .html, .css, .json, .csv';

    fileInput.addEventListener('change', async (event) => {
      const file = event.target.files[0];
      if (!file) return;

      const reader = new FileReader();
      reader.onload = async (e) => {
        const fileContent = e.target.result;
        const chunkSize = 15000;
        const chunks = [];

        // 分割文件内容为分片
        for (let i = 0; i < fileContent.length; i += chunkSize) {
          chunks.push(fileContent.slice(i, i + chunkSize));
        }

        // 逐个提交分片
        for (let i = 0; i < chunks.length; i++) {
          await submitConversation(chunks[i], i + 1, file.name);
        }
      };

      reader.readAsText(file);
    });

    fileInput.click();
  });
}

// 提交对话分片的函数
async function submitConversation(text, part, filename) {
  const textarea = document.querySelector("textarea[tabindex='0']");
  if (!textarea) return;

  // 触发输入框内容变化事件,确保ChatGPT识别输入
  textarea.value = `第${part}部分(${filename}):\n\n${text}`;
  textarea.dispatchEvent(new Event('input', { bubbles: true }));

  // 模拟按下Enter键提交
  const enterKeyEvent = new KeyboardEvent('keydown', {
    bubbles: true,
    cancelable: true,
    key: 'Enter',
    code: 'Enter',
    keyCode: 13
  });
  textarea.dispatchEvent(enterKeyEvent);

  // 等待提交完成(可根据实际情况调整延迟时间)
  await new Promise(resolve => setTimeout(resolve, 1000));
}

// 监听SPA页面的URL变化
function observeUrlChanges() {
  let currentUrl = window.location.href;

  // 监听浏览器前进后退事件
  window.addEventListener('popstate', () => {
    if (window.location.href !== currentUrl) {
      currentUrl = window.location.href;
      createUploadButton();
    }
  });

  // 重写History API方法,监听URL更新
  const originalPushState = history.pushState;
  history.pushState = function(...args) {
    originalPushState.apply(this, args);
    if (window.location.href !== currentUrl) {
      currentUrl = window.location.href;
      createUploadButton();
    }
  };

  const originalReplaceState = history.replaceState;
  history.replaceState = function(...args) {
    originalReplaceState.apply(this, args);
    if (window.location.href !== currentUrl) {
      currentUrl = window.location.href;
      createUploadButton();
    }
  };
}

// 初始化执行
document.addEventListener('DOMContentLoaded', () => {
  createUploadButton();
  observeUrlChanges();
});

// 处理DOM已加载完成的情况
if (document.readyState === 'complete' || document.readyState === 'interactive') {
  createUploadButton();
  observeUrlChanges();
}
关键改进说明
  • 修复Manifest V3配置错误,符合浏览器扩展规范
  • 新增按钮重复创建检查,避免页面出现多个按钮
  • 用MutationObserver解决目标元素未加载导致的插入失败问题
  • 监听SPA的所有URL变化场景,确保切换聊天页面时按钮正常显示
  • 优化提交逻辑,添加输入框内容变化事件,确保ChatGPT能正确识别输入内容

内容的提问来源于stack exchange,提问作者Zach Jeffries

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 03:35:03