Commit f35ba8e5 by shilin Committed by GitHub

feat(chatbot-extension): a Chrome extension that can be using for chat with AI…

feat(chatbot-extension): a Chrome extension that can be using for chat with AI on any website (#2235)

* feat(chatbot-extension): a Chrome extension that can be using for chat with AI on any website

* fix: 插件支持语音输入
feat:chatbot支持切换

* fix: 切换chatbot后,自动隐藏bot列表
parent e36d9d79
{
"manifest_version": 3,
"name": "ChatBot Extension",
"version": "1.1",
"description": "A ChatBot",
"permissions": [
"storage",
"notifications",
"tabs",
"activeTab",
"scripting",
"webRequest"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "src/background.js"
},
"action": {
"default_popup": "src/popup.html",
"default_icon": {
"16": "img/favicon32.png",
"48": "img/favicon32.png",
"128": "img/favicon32.png"
}
},
"icons": {
"16": "img/favicon32.png",
"32": "img/favicon32.png",
"48": "img/favicon32.png",
"128": "img/favicon32.png"
},
"content_scripts": [
{
"js": [
"src/content.js"
],
"matches": ["<all_urls>"]
}
]
}
\ No newline at end of file
let requestInterceptor = null;
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
if (message.action === "startRequestInterception") {
const botSrc = message.chatbotSrc;
console.log("src", botSrc);
const urlObj = new URL(botSrc);
const domain = urlObj.host;
const searchParams = urlObj.searchParams;
const frameShareId = searchParams.get('shareId') || '';
let frameChatId = searchParams.get('chatId') || '';
// 移除已有的拦截器(如果存在)
if (requestInterceptor) {
chrome.webRequest.onBeforeRequest.removeListener(requestInterceptor);
}
requestInterceptor = function (details) {
if (details.frameId !== -1
&& details.url.includes(domain)
&& details.url.includes("chat/completions")) {
console.log("Intercepted request from chatbot-iframe:", details);
if (details.requestBody.raw) {
let decoder = new TextDecoder("utf-8");
let postData = decoder.decode(new Uint8Array(details.requestBody.raw[0].bytes));
try {
let postDataObj = JSON.parse(postData);
let shareId = postDataObj.shareId;
let chatId = postDataObj.chatId;
if (frameChatId !== chatId && frameShareId === shareId) {
chrome.storage.local.set({["shareId"]: shareId});
chrome.storage.local.set({["chatId"]: chatId});
frameChatId = chatId;
console.log(`Stored shareId: ${shareId} and chatId: ${chatId} to localStorage.`);
}
} catch (error) {
console.error("Error parsing postData:", error);
}
}
}
return {};
};
chrome.webRequest.onBeforeRequest.addListener(
requestInterceptor,
{ urls: ["<all_urls>"] },
["requestBody"]
);
}
});
<!DOCTYPE html>
<html>
<head>
<title>Chatbot Extension</title>
<meta charset="UTF-8">
<style>
body,
html {
height: 600px;
width: 800px;
margin: 0;
padding: 0;
display: flex;
}
::-webkit-scrollbar {
width: 0;
}
h1 {
text-align: center;
cursor: pointer;
color: #007BFF;
}
#chatbot-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
flex-grow: 1;
}
#chatbot-iframe {
width: 100%;
height: 100%;
flex-grow: 1;
border: none;
box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1);
}
.config-btn-icon {
position: absolute;
display: none;
top: 8px;
right: 50px;
width: 30px;
height: 30px;
cursor: pointer;
}
.fullScreen-icon {
position: absolute;
display: none;
top: 8px;
right: 80px; /* Adjusted to position left of the config button */
width: 30px;
height: 30px;
cursor: pointer;
}
.overlay {
position: absolute;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
transition: opacity 0.3s ease;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.overlay-content {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-size: 18px;
color: #333;
font-weight: bold;
transition: transform 0.3s ease;
transform: scale(0.9);
}
.overlay-content.show {
transform: scale(1);
}
</style>
</head>
<body>
<div id="chatbot-container">
<div id="configOverlay" class="overlay" style="display: none;">
<div class="overlay-content">
未配置机器人,请点击右上角【设置】
</div>
</div>
<iframe id="chatbot-iframe" allowfullscreen allow="microphone"></iframe>
<!-- <webview id="chatbot-iframe" allowfullscreen></webview>-->
<img src="../img/fullScreen.png" class="fullScreen-icon" id="fullScreen">
<img src="../img/setting.png" class="config-btn-icon" id="config-btn">
</div>
<script src="popup.js"></script>
</body>
</html>
document.addEventListener('DOMContentLoaded', function () {
const chatbotIframe = document.getElementById('chatbot-iframe');
const fullScreenBtn = document.getElementById('fullScreen');
const configBtn = document.getElementById('config-btn');
const overlay = document.getElementById('configOverlay');
configBtn.addEventListener('click', function () {
window.location.href = 'setting.html'
});
// 监听 chatbotIframe 加载完成事件
chatbotIframe.addEventListener('load', function() {
// 当 iframe 加载完成后显示按钮
fullScreenBtn.style.display = 'inline-block';
configBtn.style.display = 'inline-block';
});
chrome.storage.local.get(["chatbotSrc", "shareId", "chatId", "fastUID"]).then((result) => {
const botSrc = result.chatbotSrc;
let fastUID = result.fastUID;
if (!fastUID || fastUID === '') {
fastUID = generateUUID();
chrome.storage.local.set({
fastUID: fastUID
});
}
console.log('fastUID is', fastUID);
console.log("chatbotSrc is " + botSrc);
console.log("shareId is " + result.shareId);
console.log("chatId is " + result.chatId);
chatbotIframe.src = result.chatbotSrc + "&authToken=" + fastUID;
if (!botSrc || botSrc === 'about:blank' || botSrc === '') {
overlay.style.display = 'flex';
} else {
if (botSrc.includes(result.shareId)) {
chatbotIframe.src = chatbotIframe.src + "&chatId=" + result.chatId;
console.log('chatbotIframe.src', chatbotIframe.src);
}
overlay.style.display = 'none';
chrome.runtime.sendMessage({
action: "startRequestInterception",
chatbotSrc: botSrc
});
}
});
fullScreenBtn.addEventListener('click', function () {
const iframe = document.getElementById('chatbot-iframe');
if (iframe) {
const iframeSrc = iframe.src;
console.log('fullScreenSrc', iframeSrc)
chrome.tabs.create({url: iframeSrc});
}
});
});
function generateUUID() {
const randomString = 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function () {
const randomHex = (Math.random() * 16) | 0;
return randomHex.toString(16);
});
const timestamp = Date.now().toString(16);
const extraRandom = (Math.random() * 1e16).toString(16);
return `${randomString}-${timestamp}-${extraRandom}`;
}
let chatbotSrc = '';
let configs = [];
document.addEventListener('DOMContentLoaded', async function () {
const storageData = await chrome.storage.local.get(["chatbotSrc", "configs", "showChatBot", "chatBotWidth", "chatBotHeight"]);
chatbotSrc = storageData.chatbotSrc || '';
configs = storageData.configs || [];
const showChatBot = storageData.showChatBot === undefined ? true : storageData.showChatBot;
const chatBotWidth = storageData.chatBotWidth || 400;
const chatBotHeight = storageData.chatBotHeight || 700;
await loadConfigs(configs, chatbotSrc);
document.getElementById('addConfigButton').addEventListener('click', handleAddButtonClick);
document.getElementById('startChatButton').addEventListener('click', () => window.location.href = 'popup.html');
// 监听开关和输入框变化事件
const showChatBotSwitch = document.getElementById('showChatBotSwitch');
const chatBotWidthInput = document.getElementById('chatBotWidthInput');
const chatBotHeightInput = document.getElementById('chatBotHeightInput');
showChatBotSwitch.addEventListener('change', () => handleShowChatBotChange(showChatBotSwitch.checked));
chatBotWidthInput.addEventListener('change', () => handleChatBotWidthChange(chatBotWidthInput.value));
chatBotHeightInput.addEventListener('change', () => handleChatBotHeightChange(chatBotHeightInput.value));
// 初始化开关和输入框的值
showChatBotSwitch.checked = showChatBot;
chatBotWidthInput.value = chatBotWidth;
chatBotHeightInput.value = chatBotHeight;
});
async function loadConfigs(configs, chatbotSrc) {
const configList = document.getElementById('configList');
configList.innerHTML = '';
configs.forEach(config => {
const row = createConfigRow(config, chatbotSrc);
configList.appendChild(row);
});
}
function createConfigRow(config, chatbotSrc) {
const row = document.createElement('tr');
row.innerHTML = `
<td>${config.name}</td>
<td>${config.url}</td>
<td>
<button type="button" class="editButton">编辑</button>
<span>|</span>
<button type="button" class="deleteButton">删除</button>
</td>
<td>
<label class="custom-radio">
<input type="radio" name="selectBot" class="selectBot" ${config.url === chatbotSrc ? 'checked' : ''}>
<span class="radio-mark"></span>
</label>
</td>
`;
row.querySelector('.editButton').addEventListener('click', () => handleEditButtonClick(row, config));
row.querySelector('.deleteButton').addEventListener('click', () => handleDeleteButtonClick(row, config));
row.querySelector('.selectBot').addEventListener('change', () => handleSelectBotChange(config.url));
return row;
}
async function handleDeleteButtonClick(row, config) {
const configList = document.getElementById('configList');
const index = Array.from(configList.children).indexOf(row);
const selectedUrl = config.url;
if (selectedUrl === chatbotSrc) {
chatbotSrc = 'about:blank';
await updateStorage('chatbotSrc', chatbotSrc);
await updateStorage('chatId', '');
await updateStorage('shareId', '');
}
configs.splice(index, 1);
await updateStorage('configs', configs);
row.remove();
}
async function handleSelectBotChange(url) {
chatbotSrc = url;
await updateStorage('chatbotSrc', chatbotSrc);
updateSelectedRadioButton();
}
function updateSelectedRadioButton() {
document.querySelectorAll('.selectBot').forEach(radio => {
if (radio.closest('tr').querySelector('td:nth-child(2)').textContent === chatbotSrc) {
radio.checked = true;
} else {
radio.checked = false;
}
});
}
function showError(message) {
const errorMsg = document.getElementById('errorMsg');
errorMsg.textContent = message;
errorMsg.style.opacity = 1; // 显示错误消息
// 设置一个定时器,在5秒后隐藏错误消息
setTimeout(() => {
errorMsg.style.opacity = 0; // 隐藏错误消息
}, 3000);
}
async function updateStorage(key, value) {
try {
await chrome.storage.local.set({[key]: value});
console.log(`${key} 已更新: ${value}`);
} catch (error) {
console.error(`更新 ${key} 出错:`, error);
}
}
function showEditControls(row, isNewConfig = false, config = null) {
const nameCell = row.querySelector('td:nth-child(1)');
const urlCell = row.querySelector('td:nth-child(2)');
const name = isNewConfig ? '' : config.name;
const url = isNewConfig ? '' : config.url;
nameCell.innerHTML = `<input type="text" class="editName" value="${name}">`;
urlCell.innerHTML = `<input type="text" class="editUrl" value="${url}">`;
const iconContainer = document.createElement('div');
iconContainer.style.display = 'flex';
iconContainer.style.flexDirection = 'column';
iconContainer.style.position = 'absolute';
iconContainer.style.top = '15px';
iconContainer.style.left = '-5px';
const confirmIcon = document.createElement('span');
confirmIcon.textContent = '√';
confirmIcon.style.color = 'green';
confirmIcon.style.cursor = 'pointer';
confirmIcon.style.margin = '0 0 10px 0';
confirmIcon.classList.add('icon-hover');
confirmIcon.classList.add('confirmButton');
confirmIcon.addEventListener('click', () => {
handleConfirmButtonClick(row, isNewConfig);
});
const cancelIcon = document.createElement('span');
cancelIcon.textContent = 'X';
cancelIcon.style.color = 'red';
cancelIcon.style.cursor = 'pointer';
cancelIcon.classList.add('icon-hover');
cancelIcon.classList.add('cancelButton')
cancelIcon.addEventListener('click', () => {
handleCancelButtonClick(row, isNewConfig);
});
iconContainer.appendChild(confirmIcon);
iconContainer.appendChild(cancelIcon);
const cell = row.querySelector('td:nth-child(3)');
cell.insertBefore(iconContainer, cell.firstChild);
}
function handleAddButtonClick() {
const newRow = document.createElement('tr');
newRow.innerHTML = `
<td></td>
<td></td>
<td>
</td>
<td>
</td>
`;
document.getElementById('configList').appendChild(newRow);
showEditControls(newRow, true);
}
function handleEditButtonClick(row, config) {
disableAllEditButtons();
showEditControls(row, false, config);
}
function isConfigUnique(name, url, index) {
return !configs.some((config, i) =>
i !== index && (config.name === name || config.url === url)
);
}
function checkConfig(name, url, index) {
let errorMsg = '';
if (!name || !url) {
errorMsg = '名称和地址都是必填项。';
} else if (!isConfigUnique(name, url, index)) {
errorMsg = '名称或地址不能重复。';
}
return errorMsg;
}
function handleConfirmButtonClick(row, isNewConfig = false) {
const name = row.querySelector('.editName').value;
const url = row.querySelector('.editUrl').value;
const index = isNewConfig ? -1 : Array.from(document.getElementById('configList').children).indexOf(row);
const errorMsg = checkConfig(name, url, index);
if (errorMsg) {
showError(errorMsg);
return;
}
if (isNewConfig) {
const newConfig = {name, url};
configs.push(newConfig);
updateStorage('configs', configs);
} else {
const config = configs[index];
config.name = name;
if (config.url === chatbotSrc) {
chatbotSrc = url;
updateStorage('chatId', '');
updateStorage('shareId', '');
chrome.runtime.sendMessage({
action: "startRequestInterception",
chatbotSrc: chatbotSrc
});
}
config.url = url;
updateStorage('configs', configs);
updateStorage('chatbotSrc', chatbotSrc);
}
loadConfigs(configs, chatbotSrc);
row.remove();
enableAllEditButtons();
}
function handleCancelButtonClick(row, isNewConfig = false) {
if (isNewConfig) {
row.remove();
} else {
const index = Array.from(document.getElementById('configList').children).indexOf(row);
const config = configs[index];
row.querySelector('td:nth-child(1)').textContent = config.name;
row.querySelector('td:nth-child(2)').textContent = config.url;
row.querySelector('.editButton').disabled = false;
// 移除编辑模式下的确认和取消按钮
const iconContainer = row.querySelector('td:nth-child(3) > div');
if (iconContainer) {
iconContainer.remove();
}
}
enableAllEditButtons();
}
async function handleShowChatBotChange(showChatBot) {
await updateStorage('showChatBot', showChatBot);
}
async function handleChatBotWidthChange(width) {
const parsedWidth = parseInt(width);
if (!isNaN(parsedWidth)) {
await updateStorage('chatBotWidth', parsedWidth);
}
}
async function handleChatBotHeightChange(height) {
const parsedHeight = parseInt(height);
if (!isNaN(parsedHeight)) {
await updateStorage('chatBotHeight', parsedHeight);
}
}
function disableAllEditButtons() {
const allEditButtons = document.querySelectorAll('.editButton');
allEditButtons.forEach(button => {
button.disabled = true;
});
}
function enableAllEditButtons() {
const allEditButtons = document.querySelectorAll('.editButton');
allEditButtons.forEach(button => {
button.disabled = false;
});
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment