我正在尝试为基于 FireFox/Gecko 的浏览器做一个扩展,将上传文件按钮添加到 chat.openai.com。当我添加我的扩展程序时,它只会在我刷新 1 个聊天页面时添加按钮。如果我去过去的聊天,它不会放入按钮。 (顺便说一句,我在 ChatGPT 的帮助下写了这段代码,哈哈)。
清单.json:
{
"manifest_version": 3,
"name": "ChatGPT File Upload",
"version": "1.0",
"description": "Adds a button to upload files into ChatGPT. (NOT for images, videos, Word Documents, or other non-raw-text files. Please use .txt, .js, .py, .html, .css, .json, and .csv.",
"permissions": [
"scripting",
"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": {
"scripts": ["background.js"],
"service_worker": "background.js"
}
}
背景.js:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (changeInfo.url && changeInfo.url.startsWith('https://chat.openai.com/')) {
chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['content.js']
});
}
});
content.js:
console.log("Content script loaded.");
// This script will be injected into chat.openai.com pages
// You can add your desired functionality here
// Create the button
const button = document.createElement('button');
button.innerText = '📂 Submit File';
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';
// Create a container div for centering
const containerDiv = document.createElement('div');
containerDiv.style.display = 'flex';
containerDiv.style.justifyContent = 'center';
// Append the button to the container div
containerDiv.appendChild(button);
// Find the target element
const targetElement = document.querySelector("div.relative.flex.h-full.max-w-full.flex-1.overflow-hidden > div > main > div.absolute.bottom-0 > form > div > div:nth-child(1)");
// Insert the container div before the target element
targetElement.parentNode.insertBefore(containerDiv, targetElement);
// Add click event listener to the button
button.addEventListener('click', async () => {
// Create the file input element
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.txt, .js, .py, .html, .css, .json, .csv';
// Handle file selection
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
const fileContent = e.target.result;
const chunkSize = 15000;
const chunks = [];
// Split file content into chunks
for (let i = 0; i < fileContent.length; i += chunkSize) {
const chunk = fileContent.slice(i, i + chunkSize);
chunks.push(chunk);
}
// Submit each chunk to the conversation
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const part = i + 1;
const filename = file.name;
await submitConversation(chunk, part, filename);
}
};
reader.readAsText(file);
}
});
// Trigger file input click event
fileInput.click();
});
// Submit conversation function
async function submitConversation(text, part, filename) {
const textarea = document.querySelector("textarea[tabindex='0']");
const enterKeyEvent = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
keyCode: 13,
});
textarea.value = `Part ${part} of ${filename}:\n\n${text}`;
textarea.dispatchEvent(enterKeyEvent);
}
我浏览了网上找到的不同 background.js,但似乎没有一个能解决我的问题。我对开发很陌生,所以我大部分时间都迷失在这种事情上。
在这里扩展我的评论。
可以有两种类型的导航
在第一种情况下,扩展程序将完成其工作并加载按钮,但在第二种情况下,如果网络应用程序正在使用新元素加载整个页面,那么您的“编辑”元素将被替换为新元素。
你可以通过两种方式解决这个问题(AFAIK)
听导航事件
如果网络应用程序正在更改地址栏中的地址,那么您可以收听该事件并添加按钮(如果尚不存在)。 https://developer.mozilla.org/zh-CN/docs/Web/API/Navigation/navigate_event
navigation.addEventListener("navigate", (event) => {
checkAndInjectButton()
})
突变观察者
如果由于某种原因您无法检测到网络应用程序中的导航变化,那么您可以监听 DOM 中的变化并根据事件做出反应。
Mutation oberver 跟踪属性、子节点和子树,因此如果对目标元素进行任何更改,您的脚本将获得回调。
// Select the node that will be observed for mutations
const targetElement = document.querySelector("div.relative.flex.h-full.max-w-full.flex-1.overflow-hidden > div > main > div.absolute.bottom-0 > form > div > div:nth-child(1)");
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = (mutationList, observer) => {
checkAndInjectButton();
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetElement, config);
// If you need to stop observing
observer.disconnect();
这两种解决方案都会转到您的
content.js
我将使用以下方法发布程序的框架:“监听导航更改”。这很复杂,但我没有找到任何更简单的解决方案。该代码是正在运行的 firefox 附加组件的简化部分。制作该插件的原因是帮助用户减少在内联网页面中的点击次数(自动化)。您需要“权限”下的“webRequest”:[]。我使用“清单版本”:2
/* CONTENT SCRIPT anyname.js */
"use strict"
function listenAny(I, sender, outcom) {
if (I.what == SID_REQ_COMPLETE) {
//Here listen for messages from background.js //from line with //@@1
//the page maybe load something by ajax call
DoAnythingInPage(2) // 2 => reason is PAGE MODIFIED
}
}
browser.runtime.onMessage.addListener(listenAny)
function start() {
//send message to background and wait a response to take the information:
//in whitch tab this script is loaded (via Response.tabid)
browser.runtime.sendMessage({ what: SID_LOADING }).then((Response) => {
//here the script knows in whitch tab is executing
DoAnythingInPage(1) // 1 => reason is PAGE LOADING
}).catch((e) => {
//
})
}
function DoAnythingInPage(reason) {
//HERE WRITE THE CODE TO MODIFY THE PAGE. THE reason MEANS
// a)the page is loading of xmlhttprequest or b) some partial loading happans, maybe ajax request
// You will deside what to do in any case
}
//when the page loads this is the first line of code to execute:
start()
/* BACKGROUND.JS*/
"use strict"
let tabsIwant = []
const urlFilter = {
urls: ['https://chat.openai.com/*','https://anything.else.com/*']
}
function back_listenAny(rq, sender, resp) {
if (rq.what == SID_LOADING) {
//in an array store all tabs in whitch the content script loads
tabsIwant.push(sender.tab.id)
resp({ tabid: sender.tab.id })
}
}
function removeTab(tabId) {
let i = tabsIwant.indexOf(tabId)
if (i >= 0) {
tabsIwant.splice(i, 1)
}
}
function onReqComplete(rDtls) {
if (rDtls.fromCache || (rDtls.type != 'xmlhttprequest') || rDtls.tabId == -1 ) return
//in line above you can eliminate firing the SID_REQ_COMPLETE to content script
//after analysing the requests from browser tools.
//In my case I did the following additional checks
/*|| rDtls.url.search('/images/') >= 0 || rDtls.url.search('/resources/') >= 0 */
//check if this tab is in your interests...
//you don't need this check (and all the code that supports the array tabsIwant) if the urlFilter
//contains only one domain
if (tabsIwant.indexOf(rDtls.tabId) >= 0) {
browser.tabs.sendMessage(rDtls.tabId, { what: SID_REQ_COMPLETE, rDtls: rDtls }) //@@1
.catch((e) => {
//...
})
}
}
browser.tabs.onRemoved.addListener((tabId, removeInfo) => {
removeTab(tabId)
})
browser.webRequest.onCompleted.addListener(onReqComplete, urlFilter)