diff --git a/README.md b/README.md index bc9ad72..98bd018 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ -# AI 圆桌 (AI Roundtable) +# AI 圆桌 中国版 (AI Roundtable CN) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Status: Experimental](https://img.shields.io/badge/Status-Experimental-orange.svg)](#-experimental-prototype--实验性原型) +[![Version](https://img.shields.io/badge/Version-0.5.4-brightgreen.svg)](https://github.com/firebear/ai-roundtable-cn) +[![Status: Stable](https://img.shields.io/badge/Status-Stable-success.svg)](#-稳定版本) -> 让多个 AI 助手围桌讨论,交叉评价,深度协作 +> 让多个 AI 助手围桌讨论,交叉评价,深度协作 - 支持7个主流AI平台 -一个 Chrome 扩展,让你像"会议主持人"一样,同时操控多个 AI(Claude、ChatGPT、Gemini),实现真正的 AI 圆桌会议。 +一个 Chrome 扩展,让你像"会议主持人"一样,同时操控多个 AI(Claude、ChatGPT、Gemini、DeepSeek、Kimi、ChatGLM、通义千问),实现真正的 AI 圆桌会议。 + +**中国版特色**:完整支持4个国产大模型(DeepSeek、Kimi、ChatGLM、通义千问),让AI圆桌更适合中国用户。 @@ -86,6 +89,20 @@ I'm currently most satisfied with, and calibrated to, the **web chat experience* - **交叉引用** - 让 Claude 评价 ChatGPT 的回答,或反过来 - **讨论模式** - 两个 AI 就同一主题进行多轮深度讨论 - **无需 API** - 直接操作网页界面,使用你现有的 AI 订阅 +- **国产大模型支持** - 新增DeepSeek等国产AI平台,更适合中国用户 + +## 支持平台 + +**国际AI**: +- ✅ **Claude** - 完全自动化支持 +- ✅ **ChatGPT** - 完全自动化支持 +- ✅ **Gemini** - 完全自动化支持 + +**国产AI**: +- ✅ **DeepSeek** - 完全自动化支持(Enter键发送) +- ✅ **Kimi** - 完全自动化支持(Clipboard API + 打字模拟) +- ✅ **ChatGLM** - 完全自动化支持 +- ✅ **通义千问** - 完全自动化支持 --- @@ -129,11 +146,19 @@ I'm currently most satisfied with, and calibrated to, the **web chat experience* ### 准备工作 1. 打开 Chrome,登录以下 AI 平台(根据需要): + + **国际AI**: - [Claude](https://claude.ai) - [ChatGPT](https://chatgpt.com) - [Gemini](https://gemini.google.com) -2. 推荐使用 Chrome 的 Split Tab 功能,将 2 个 AI 页面并排显示 + **国产AI** 🆕: + - [DeepSeek](https://chat.deepseek.com) + - [Kimi](https://kimi.moonshot.cn) + - [ChatGLM](https://chatglm.cn) + - [通义千问](https://tongyi.aliyun.com) + +2. 推荐使用 Chrome 的 Split Tab 功能,将 2-7 个 AI 页面并排显示 3. 点击扩展图标,打开侧边栏控制台 @@ -144,13 +169,13 @@ I'm currently most satisfied with, and calibrated to, the **web chat experience* ### 普通模式 **基本发送** -1. 勾选要发送的目标 AI(Claude / ChatGPT / Gemini) +1. 勾选要发送的目标 AI(默认:DeepSeek、Kimi) 2. 输入消息 3. 按 Enter 或点击「发送」按钮 **@ 提及语法** - 点击 @ 按钮快速插入 AI 名称 -- 或手动输入:`@Claude 你怎么看这个问题?` +- 或手动输入:`@Claude 你怎么看这个问题?`、`@DeepSeek 分析一下`、`@Kimi 你的观点呢?` **互评(推荐)** @@ -207,7 +232,7 @@ I'm currently most satisfied with, and calibrated to, the **web chat experience* ## 技术架构 ``` -ai-roundtable/ +ai-roundtable-cn/ ├── manifest.json # Chrome 扩展配置 (Manifest V3) ├── background.js # Service Worker 消息中转 ├── sidepanel/ @@ -217,7 +242,12 @@ ai-roundtable/ ├── content/ │ ├── claude.js # Claude 页面注入脚本 │ ├── chatgpt.js # ChatGPT 页面注入脚本 -│ └── gemini.js # Gemini 页面注入脚本 +│ ├── gemini.js # Gemini 页面注入脚本 +│ ├── deepseek.js # DeepSeek 页面注入脚本 +│ ├── kimi.js # Kimi 页面注入脚本 🆕 +│ ├── chatglm.js # ChatGLM 页面注入脚本 🆕 +│ └── qwen.js # 通义千问页面注入脚本 🆕 +├── 版本日志.md # 版本更新记录 └── icons/ # 扩展图标 ``` @@ -274,17 +304,25 @@ MIT License - see [LICENSE](LICENSE) for details. ## Author -**Axton Liu** - AI Educator & Creator +**原项目**: [Axton Liu](https://github.com/axtonliu) - AI Educator & Creator - Website: [axtonliu.ai](https://www.axtonliu.ai) - YouTube: [@AxtonLiu](https://youtube.com/@AxtonLiu) - Twitter/X: [@axtonliu](https://twitter.com/axtonliu) +- Learn More: [AI Elite Weekly Newsletter](https://www.axtonliu.ai/newsletters/ai-2) -### Learn More +**中国版 (Fork)**: [firebear](https://github.com/firebear) -- [AI Elite Weekly Newsletter](https://www.axtonliu.ai/newsletters/ai-2) - Weekly AI insights -- [Free AI Course](https://www.axtonliu.ai/axton-free-course) - Get started with AI +- GitHub: [@firebear](https://github.com/firebear) +- Repository: [AI Roundtable CN](https://github.com/firebear/ai-roundtable-cn) + +**中国版更新**: +- 新增DeepSeek等国产大模型支持 +- 完善中文文档和使用指南 +- 优化中国用户体验 --- -© AXTONLIU™ & AI 精英学院™ 版权所有 +原项目 © AXTONLIU™ & AI 精英学院™ 版权所有 + +中国版修改部分遵循相同MIT许可证 diff --git a/background.js b/background.js index dc50c7e..957add7 100644 --- a/background.js +++ b/background.js @@ -4,13 +4,25 @@ const AI_URL_PATTERNS = { claude: ['claude.ai'], chatgpt: ['chat.openai.com', 'chatgpt.com'], - gemini: ['gemini.google.com'] + gemini: ['gemini.google.com'], + deepseek: ['chat.deepseek.com'], + kimi: ['kimi.moonshot.cn', 'www.kimi.com'], + chatglm: ['chatglm.cn'], + qwen: ['tongyi.aliyun.com', 'www.qianwen.com'] }; // Store latest responses using chrome.storage.session (persists across service worker restarts) async function getStoredResponses() { const result = await chrome.storage.session.get('latestResponses'); - return result.latestResponses || { claude: null, chatgpt: null, gemini: null }; + return result.latestResponses || { + claude: null, + chatgpt: null, + gemini: null, + deepseek: null, + kimi: null, + chatglm: null, + qwen: null + }; } async function setStoredResponse(aiType, content) { diff --git a/content/chatglm.js b/content/chatglm.js new file mode 100644 index 0000000..e5e2901 --- /dev/null +++ b/content/chatglm.js @@ -0,0 +1,450 @@ +// AI Panel - ChatGLM Content Script + +(function() { + 'use strict'; + + const AI_TYPE = 'chatglm'; + + // State tracking + let isSending = false; + let latestResponse = ''; + + // Check if extension context is still valid + function isContextValid() { + return chrome.runtime && chrome.runtime.id; + } + + // Safe message sender that checks context first + function safeSendMessage(message, callback) { + if (!isContextValid()) { + console.log('[AI Panel] Extension context invalidated, skipping message'); + return; + } + try { + chrome.runtime.sendMessage(message, callback); + } catch (e) { + console.log('[AI Panel] Failed to send message:', e.message); + } + } + + // Notify background that content script is ready + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + + // Listen for messages from background script + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'INJECT_MESSAGE') { + injectMessage(message.message) + .then(() => { + sendResponse({ success: true }); + }) + .catch(err => { + console.error('[AI Panel] ChatGLM injection error:', err); + sendResponse({ success: false, error: err.message }); + }); + return true; // Keep channel open for async response + } + + if (message.type === 'GET_LATEST_RESPONSE') { + const response = getLatestResponse(); + sendResponse({ content: response }); + return true; + } + + return false; + }); + + // Setup response observer for cross-reference feature + setupResponseObserver(); + + // Ensure content script is ready even if page loads dynamically + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + }); + } else { + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + } + + async function injectMessage(text) { + // Prevent duplicate sending + if (isSending) { + return false; + } + isSending = true; + + try { + // ChatGLM uses textarea for input + const inputSelectors = [ + 'textarea[placeholder*="输入"]', + 'textarea[placeholder*="message"]', + 'textarea[placeholder*="Message"]', + 'textarea[placeholder*="问智谱"]', + 'textarea[placeholder*="和GLM"]', + 'textarea[class*="input"]', + 'textarea[class*="textarea"]', + 'div[contenteditable="true"]', + 'textarea' + ]; + + let inputEl = null; + for (const selector of inputSelectors) { + inputEl = document.querySelector(selector); + if (inputEl && isVisible(inputEl)) { + break; + } + } + + if (!inputEl) { + throw new Error('Could not find input field'); + } + + // Focus the input + inputEl.focus(); + + // Handle different input types + if (inputEl.tagName === 'TEXTAREA') { + // For textarea, use value setter + inputEl.value = text; + + // Trigger React/Vue change events + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + )?.set; + + if (nativeInputValueSetter) { + nativeInputValueSetter.call(inputEl, text); + } + + // Dispatch events to trigger UI updates + inputEl.dispatchEvent(new Event('input', { bubbles: true })); + inputEl.dispatchEvent(new Event('change', { bubbles: true })); + inputEl.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true })); + } else { + // Contenteditable div + inputEl.textContent = text; + inputEl.innerText = text; + inputEl.dispatchEvent(new Event('input', { bubbles: true })); + inputEl.dispatchEvent(new Event('keyup', { bubbles: true })); + } + + // Small delay to let the UI process + await sleep(300); + + // Find the send button + const sendButton = findSendButton(); + if (sendButton) { + // Try to click send button + await clickSendButton(sendButton); + } + + // Start capturing response + setTimeout(() => { + waitForStreamingComplete(); + }, 2000); + + return true; + } finally { + // Reset sending state after a delay + setTimeout(() => { + isSending = false; + }, 1000); + } + } + + async function clickSendButton(button) { + // Try multiple methods to click the button + try { + // If it's an SVG element, click its parent container + if (button.tagName === 'svg' || button.tagName === 'path') { + const container = button.closest('[class*="button"]') || button.closest('button') || button.parentElement; + if (container) { + + // Method 1: Click container + container.click(); + await sleep(200); + + // Method 2: Click SVG itself + button.click(); + await sleep(100); + + // Method 3: Mouse events on container + const rect = container.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + container.dispatchEvent(new MouseEvent('mousedown', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0, + buttons: 1 + })); + await sleep(50); + container.dispatchEvent(new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0, + buttons: 0 + })); + await sleep(50); + container.dispatchEvent(new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0 + })); + + return; + } + } + + // Standard button click + + // Method 1: Standard click + button.click(); + await sleep(200); + + // Method 2: Mouse events + const rect = button.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + const mouseDown = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0, + buttons: 1 + }); + const mouseUp = new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0, + buttons: 0 + }); + const clickEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0 + }); + + button.dispatchEvent(mouseDown); + await sleep(50); + button.dispatchEvent(mouseUp); + await sleep(50); + button.dispatchEvent(clickEvent); + } catch (err) { + console.error('[AI Panel] ChatGLM: Click error:', err); + // Highlight button if click fails + const buttonToHighlight = button.tagName === 'svg' ? button.parentElement : button; + if (buttonToHighlight) { + buttonToHighlight.style.boxShadow = '0 0 10px 3px rgba(66, 153, 225, 0.6)'; + buttonToHighlight.style.transition = 'box-shadow 0.3s'; + setTimeout(() => { + buttonToHighlight.style.boxShadow = ''; + }, 3000); + } + } + } + + function findSendButton() { + // ChatGLM send button selectors + const selectors = [ + 'div.enter-icon-container:not(.empty) img.enter_icon', // ChatGLM specific - enabled + 'div.enter-icon-container img.enter_icon', // ChatGLM specific - any state + 'div[class*="enter-icon"] img[src*="send"]', // Partial class match + 'div.enter-icon-container', // Container itself + 'button[aria-label="Send"]', + 'button[aria-label="send"]', + 'button[aria-label="发送"]', + 'button[type="submit"]', + 'button[class*="send"]', + 'button[class*="submit"]', + 'div[role="button"][class*="send"]', + 'button:has(svg)', + 'svg[class*="send"]' + ]; + + for (const selector of selectors) { + const btn = document.querySelector(selector); + if (btn && isVisible(btn)) { + // If it's the img element, return its container + if (btn.tagName === 'IMG') { + const container = btn.closest('.enter-icon-container'); + if (container) { + return container; + } + } + return btn; + } + } + + // Fallback: try to find any button that looks like a send button + const buttons = Array.from(document.querySelectorAll('button, div[class*="icon"], img[src*="send"]')); + + for (const btn of buttons) { + const text = btn.textContent?.toLowerCase() || ''; + const className = btn.className?.toLowerCase() || ''; + + if (text.includes('send') || text.includes('发送') || + text.includes('提交') || text === '新对话' || + className.includes('enter-icon') || className.includes('send') || + className.includes('submit') || (btn.tagName === 'IMG' && btn.src?.includes('send'))) { + if (isVisible(btn)) { + // If it's img, return container + if (btn.tagName === 'IMG') { + const container = btn.closest('.enter-icon-container'); + if (container) return container; + } + return btn; + } + } + } + + console.error('[AI Panel] ChatGLM: No send button found!'); + return null; + } + + function setupResponseObserver() { + // ChatGLM response detection + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.addedNodes.length > 0) { + checkForNewMessages(); + } + } + }); + + // Start observing when DOM is ready + const startObserving = () => { + const chatContainer = findChatContainer(); + if (chatContainer) { + observer.observe(chatContainer, { + childList: true, + subtree: true + }); + } + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', startObserving); + } else { + startObserving(); + } + } + + function findChatContainer() { + // ChatGLM chat container selectors + const selectors = [ + 'div[class*="chat"]', + 'div[class*="message"]', + 'div[class*="conversation"]', + 'main', + '[role="main"]', + 'div[id*="chat"]', + 'div[id*="message"]' + ]; + + for (const selector of selectors) { + const container = document.querySelector(selector); + if (container) { + return container; + } + } + + return document.body; + } + + function checkForNewMessages() { + // Find assistant messages (ChatGLM's responses) + const messageSelectors = [ + 'div[class*="message"][class*="assistant"]', + 'div[class*="chat"] div[class*="assistant"]', + 'div[data-message-role="assistant"]', + 'div[class*="markdown"]', + 'div[class*="markdown-prose"]', + 'article', + '[data-testid*="assistant"]', + '[data-message-id]' + ]; + + for (const selector of messageSelectors) { + const messages = document.querySelectorAll(selector); + if (messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + const text = extractText(lastMessage); + + if (text && text.length > 0) { + latestResponse = text; + } + } + } + } + + function extractText(element) { + // Extract text content, handling potential nested structures + let text = element.textContent || element.innerText || ''; + return text.trim(); + } + + function getLatestResponse() { + return latestResponse; + } + + function waitForStreamingComplete() { + // Wait for response to complete (ChatGLM streams responses) + let lastLength = 0; + let stableCount = 0; + + const checkInterval = setInterval(() => { + checkForNewMessages(); + + const currentLength = latestResponse.length; + + if (currentLength === lastLength) { + stableCount++; + // If content hasn't changed for 10 checks (2 seconds), consider complete + if (stableCount >= 10) { + clearInterval(checkInterval); + } + } else { + stableCount = 0; + lastLength = currentLength; + } + }, 200); + + // Timeout after 2 minutes + setTimeout(() => { + clearInterval(checkInterval); + }, 120000); + } + + function isVisible(element) { + if (!element) return false; + const style = window.getComputedStyle(element); + return style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0' && + element.offsetParent !== null; + } + + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } +})(); diff --git a/content/deepseek.js b/content/deepseek.js new file mode 100644 index 0000000..7e12d9d --- /dev/null +++ b/content/deepseek.js @@ -0,0 +1,400 @@ +// AI Panel - DeepSeek Content Script + +(function() { + 'use strict'; + + const AI_TYPE = 'deepseek'; + + // State tracking + let isSending = false; + let latestResponse = ''; + + // Check if extension context is still valid + function isContextValid() { + return chrome.runtime && chrome.runtime.id; + } + + // Safe message sender that checks context first + function safeSendMessage(message, callback) { + if (!isContextValid()) { + console.log('[AI Panel] Extension context invalidated, skipping message'); + return; + } + try { + chrome.runtime.sendMessage(message, callback); + } catch (e) { + console.log('[AI Panel] Failed to send message:', e.message); + } + } + + // Notify background that content script is ready + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + + // Listen for messages from background script + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'INJECT_MESSAGE') { + injectMessage(message.message) + .then(() => { + sendResponse({ success: true }); + }) + .catch(err => { + console.error('[AI Panel] DeepSeek injection error:', err); + sendResponse({ success: false, error: err.message }); + }); + return true; // Keep channel open for async response + } + + if (message.type === 'GET_LATEST_RESPONSE') { + const response = getLatestResponse(); + sendResponse({ content: response }); + return true; + } + + return false; + }); + + // Setup response observer for cross-reference feature + setupResponseObserver(); + + // Ensure content script is ready even if page loads dynamically + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + }); + } else { + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + } + + async function injectMessage(text) { + // Prevent duplicate sending + if (isSending) { + return false; + } + isSending = true; + + try { + // DeepSeek uses textarea for input + const inputSelectors = [ + 'textarea[placeholder*="输入"]', + 'textarea[placeholder*="message"]', + 'textarea[placeholder*="Message"]', + 'textarea[placeholder*="问DeepSeek"]', + 'textarea[class*="input"]', + 'textarea[class*="textarea"]', + 'div[contenteditable="true"]', + 'textarea' + ]; + + let inputEl = null; + for (const selector of inputSelectors) { + inputEl = document.querySelector(selector); + if (inputEl && isVisible(inputEl)) { + break; + } + } + + if (!inputEl) { + throw new Error('Could not find input field'); + } + + // Focus the input + inputEl.focus(); + + // Handle different input types + if (inputEl.tagName === 'TEXTAREA') { + // For textarea, use value setter + inputEl.value = text; + + // Trigger React/Vue change events + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + )?.set; + + if (nativeInputValueSetter) { + nativeInputValueSetter.call(inputEl, text); + } + + // Dispatch events to trigger UI updates + inputEl.dispatchEvent(new Event('input', { bubbles: true })); + inputEl.dispatchEvent(new Event('change', { bubbles: true })); + inputEl.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true })); + } else { + // Contenteditable div + inputEl.textContent = text; + inputEl.innerText = text; + inputEl.dispatchEvent(new Event('input', { bubbles: true })); + inputEl.dispatchEvent(new Event('keyup', { bubbles: true })); + } + + // Small delay to let the UI process + await sleep(300); + + // Try Method 1: Press Enter in input field (most chat apps support this) + const enterSuccess = await tryPressEnter(inputEl); + if (!enterSuccess) { + console.log('[AI Panel] DeepSeek: Enter key failed, trying click methods...'); + + // Method 2: Find and click the send button + const sendButton = findSendButton(); + if (sendButton) { + const clickSuccess = await tryClickSendButton(sendButton); + + if (!clickSuccess) { + // Fallback: Highlight the button to indicate user should click it + sendButton.style.boxShadow = '0 0 10px 3px rgba(66, 153, 225, 0.6)'; + sendButton.style.transition = 'box-shadow 0.3s'; + + setTimeout(() => { + sendButton.style.boxShadow = ''; + }, 3000); + } + } + } + + // Start capturing response + setTimeout(() => { + waitForStreamingComplete(); + }, 2000); + + return true; + } finally { + // Reset sending state after a delay + setTimeout(() => { + isSending = false; + }, 1000); + } + } + + function findSendButton() { + // DeepSeek's send button selectors + const selectors = [ + 'div.ds-icon-button[role="button"]', // DeepSeek specific + 'div[class*="ds-icon-button"][role="button"]', // Partial class match + 'div[class*="ds-icon-button"]', // Any ds-icon-button + 'button[aria-label="Send"]', + 'button[aria-label="send"]', + 'button[type="submit"]', + 'button[class*="send"]', + 'button[class*="submit"]', + 'div[role="button"][class*="send"]', + 'svg[class*="send"]', + 'button:has(svg)' // Button containing SVG icon + ]; + + for (const selector of selectors) { + const btn = document.querySelector(selector); + if (btn && isVisible(btn)) { + return btn; + } + } + + // Fallback: try to find any button that looks like a send button + const buttons = Array.from(document.querySelectorAll('button, div[role="button"]')); + + for (const btn of buttons) { + const text = btn.textContent?.toLowerCase() || ''; + const className = btn.className?.toLowerCase() || ''; + + if (text.includes('send') || text.includes('发送') || + text.includes('提交') || text === '' || // Icon button + className.includes('send') || className.includes('submit') || + className.includes('icon') || className.includes('button') || + className.includes('ds-icon')) { + if (isVisible(btn)) { + return btn; + } + } + } + + return null; + } + + async function tryPressEnter(inputElement) { + console.log('[AI Panel] DeepSeek: Trying Enter key in input field...'); + + try { + // Make sure input is focused + inputElement.focus(); + await sleep(50); + + // Method 1: Simple Enter key press + const enterEvent = new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true, + cancelable: true, + composed: true + }); + + inputElement.dispatchEvent(enterEvent); + await sleep(50); + + // Keyup + inputElement.dispatchEvent(new KeyboardEvent('keyup', { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true, + cancelable: true, + composed: true + })); + await sleep(100); + + // Keypress (deprecated but might be needed) + inputElement.dispatchEvent(new KeyboardEvent('keypress', { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true, + cancelable: true, + composed: true + })); + await sleep(200); + + console.log('[AI Panel] DeepSeek: Enter key pressed in input field'); + return true; + } catch (e) { + console.error('[AI Panel] DeepSeek: Enter key error:', e); + return false; + } + } + + async function tryClickSendButton(button) { + // All click methods have been tested and failed + // The working method is pressing Enter in the input field + // This function is kept as a fallback but will return false + return false; + } + + function setupResponseObserver() { + // DeepSeek response detection + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.addedNodes.length > 0) { + checkForNewMessages(); + } + } + }); + + // Start observing when DOM is ready + const startObserving = () => { + const chatContainer = findChatContainer(); + if (chatContainer) { + observer.observe(chatContainer, { + childList: true, + subtree: true + }); + } + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', startObserving); + } else { + startObserving(); + } + } + + function findChatContainer() { + // DeepSeek chat container selectors + const selectors = [ + 'div[class*="chat"]', + 'div[class*="message"]', + 'div[class*="conversation"]', + 'main', + '[role="main"]', + 'div[id*="chat"]', + 'div[id*="message"]' + ]; + + for (const selector of selectors) { + const container = document.querySelector(selector); + if (container) { + return container; + } + } + + return document.body; + } + + function checkForNewMessages() { + // Find assistant messages (DeepSeek's responses) + const messageSelectors = [ + 'div[class*="message"][class*="assistant"]', + 'div[class*="chat"] div[class*="assistant"]', + 'div[data-message-role="assistant"]', + 'div[class*="markdown"]', // DeepSeek uses markdown for responses + 'div[class*="markdown-prose"]', // Common markdown class + 'article', // Some sites use article tag + '[data-testid*="assistant"]', // Test ID based + '[data-message-id]' // Message ID based + ]; + + for (const selector of messageSelectors) { + const messages = document.querySelectorAll(selector); + if (messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + const text = extractText(lastMessage); + + if (text && text.length > 0) { + latestResponse = text; + } + } + } + } + + function extractText(element) { + // Extract text content, handling potential nested structures + let text = element.textContent || element.innerText || ''; + return text.trim(); + } + + function getLatestResponse() { + return latestResponse; + } + + function waitForStreamingComplete() { + // Wait for response to complete (DeepSeek streams responses) + let lastLength = 0; + let stableCount = 0; + + const checkInterval = setInterval(() => { + checkForNewMessages(); + + const currentLength = latestResponse.length; + + if (currentLength === lastLength) { + stableCount++; + // If content hasn't changed for 10 checks (2 seconds), consider complete + if (stableCount >= 10) { + clearInterval(checkInterval); + } + } else { + stableCount = 0; + lastLength = currentLength; + } + }, 200); + + // Timeout after 2 minutes + setTimeout(() => { + clearInterval(checkInterval); + }, 120000); + } + + function isVisible(element) { + if (!element) return false; + const style = window.getComputedStyle(element); + return style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0' && + element.offsetParent !== null; + } + + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } +})(); diff --git a/content/kimi.js b/content/kimi.js new file mode 100644 index 0000000..dcdc59f --- /dev/null +++ b/content/kimi.js @@ -0,0 +1,513 @@ +// AI Panel - Kimi Content Script + +(function() { + 'use strict'; + + const AI_TYPE = 'kimi'; + + // State tracking + let isSending = false; + let latestResponse = ''; + + // Check if extension context is still valid + function isContextValid() { + return chrome.runtime && chrome.runtime.id; + } + + // Safe message sender that checks context first + function safeSendMessage(message, callback) { + if (!isContextValid()) { + console.log('[AI Panel] Extension context invalidated, skipping message'); + return; + } + try { + chrome.runtime.sendMessage(message, callback); + } catch (e) { + console.log('[AI Panel] Failed to send message:', e.message); + } + } + + // Notify background that content script is ready + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + + // Listen for messages from background script + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'INJECT_MESSAGE') { + injectMessage(message.message) + .then(() => { + sendResponse({ success: true }); + }) + .catch(err => { + console.error('[AI Panel] Kimi injection error:', err); + sendResponse({ success: false, error: err.message }); + }); + return true; // Keep channel open for async response + } + + if (message.type === 'GET_LATEST_RESPONSE') { + const response = getLatestResponse(); + sendResponse({ content: response }); + return true; + } + + return false; + }); + + // Setup response observer for cross-reference feature + setupResponseObserver(); + + // Ensure content script is ready even if page loads dynamically + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + }); + } else { + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + } + + async function injectMessage(text) { + // Prevent duplicate sending + if (isSending) { + return false; + } + isSending = true; + + try { + // Kimi uses contenteditable div for input + const inputSelectors = [ + 'div.chat-input-editor[contenteditable="true"]', // Kimi specific + 'div[contenteditable="true"][data-lexical-editor="true"]', // Lexical editor + 'div[role="textbox"][contenteditable="true"]', // Generic textbox + 'div[class*="chat-input-editor"][contenteditable="true"]', // Partial class match + 'div[contenteditable="true"].chat-input-editor', // Alternative format + 'textarea[placeholder*="输入"]', + 'textarea[placeholder*="message"]', + 'textarea[placeholder*="问Kimi"]', + 'div[contenteditable="true"]' + ]; + + let inputEl = null; + + for (const selector of inputSelectors) { + const el = document.querySelector(selector); + if (el && isVisible(el)) { + inputEl = el; + break; + } + } + + if (!inputEl) { + throw new Error('Could not find input field'); + } + + // Focus the input + inputEl.focus(); + + // Handle different input types + if (inputEl.tagName === 'TEXTAREA') { + // For textarea, use value setter + inputEl.value = text; + + // Trigger React/Vue change events + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + )?.set; + + if (nativeInputValueSetter) { + nativeInputValueSetter.call(inputEl, text); + } + + // Dispatch events to trigger UI updates + inputEl.dispatchEvent(new Event('input', { bubbles: true })); + inputEl.dispatchEvent(new Event('change', { bubbles: true })); + inputEl.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true })); + } else { + // Contenteditable div - use Clipboard API for Lexical editor + inputEl.focus(); + + // Use Clipboard API to paste text + if (navigator.clipboard && window.ClipboardItem) { + try { + // Copy text to clipboard + await navigator.clipboard.write([ + new ClipboardItem({ + 'text/plain': new Blob([text], { type: 'text/plain' }) + }) + ]); + + // Trigger paste + await sleep(100); + + // Execute paste command + const pasteEvent = new ClipboardEvent('paste', { + bubbles: true, + cancelable: true, + dataType: 'text/plain', + data: text + }); + + inputEl.dispatchEvent(pasteEvent); + + // Also try document.execCommand as fallback + document.execCommand('paste'); + } catch (err) { + // Fallback to manual typing simulation + await simulateTyping(inputEl, text); + } + } else { + // Fallback: simulate typing + await simulateTyping(inputEl, text); + } + } + + // Small delay to let the UI process + await sleep(300); + + // Find the send button + const sendButton = findSendButton(); + if (sendButton) { + // Try to click send button + await clickSendButton(sendButton); + } + + // Start capturing response + setTimeout(() => { + waitForStreamingComplete(); + }, 2000); + + return true; + } finally { + // Reset sending state after a delay + setTimeout(() => { + isSending = false; + }, 1000); + } + } + + async function clickSendButton(button) { + // Try multiple methods to click the button + try { + // If it's an SVG element, click its parent container + if (button.tagName === 'svg' || button.tagName === 'path') { + const container = button.closest('.send-button-container') || button.parentElement; + if (container) { + + // Method 1: Click container + container.click(); + await sleep(200); + + // Method 2: Click SVG itself + button.click(); + await sleep(100); + + // Method 3: Mouse events on container + const rect = container.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + container.dispatchEvent(new MouseEvent('mousedown', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0, + buttons: 1 + })); + await sleep(50); + container.dispatchEvent(new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0, + buttons: 0 + })); + await sleep(50); + container.dispatchEvent(new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0 + })); + + return; + } + } + + // Standard button click + + // Method 1: Standard click + button.click(); + await sleep(200); + + // Method 2: Mouse events + const rect = button.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + const mouseDown = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0, + buttons: 1 + }); + const mouseUp = new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0, + buttons: 0 + }); + const clickEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0 + }); + + button.dispatchEvent(mouseDown); + await sleep(50); + button.dispatchEvent(mouseUp); + await sleep(50); + button.dispatchEvent(clickEvent); + } catch (err) { + console.error('[AI Panel] Kimi: Click error:', err); + // Highlight button if click fails + const buttonToHighlight = button.tagName === 'svg' ? button.parentElement : button; + if (buttonToHighlight) { + buttonToHighlight.style.boxShadow = '0 0 10px 3px rgba(66, 153, 225, 0.6)'; + buttonToHighlight.style.transition = 'box-shadow 0.3s'; + setTimeout(() => { + buttonToHighlight.style.boxShadow = ''; + }, 3000); + } + } + } + + function findSendButton() { + // Kimi send button selectors + const selectors = [ + 'div.send-button-container:not(.disabled) svg.send-icon', // Kimi specific - enabled button + 'div.send-button-container svg.send-icon', // Kimi specific - any state + 'button[aria-label="Send"]', + 'button[aria-label="send"]', + 'button[aria-label="发送"]', + 'button[type="submit"]', + 'button[class*="send"]', + 'button[class*="submit"]', + 'div[role="button"][class*="send"]', + 'button:has(svg)' + ]; + + for (const selector of selectors) { + const btn = document.querySelector(selector); + if (btn && isVisible(btn)) { + return btn; + } + } + + // Fallback: try to find any button that looks like a send button + const buttons = Array.from(document.querySelectorAll('button, div[role="button"], svg[class*="send"]')); + + for (const btn of buttons) { + const text = btn.textContent?.toLowerCase() || ''; + const className = btn.className?.toLowerCase() || ''; + + if (text.includes('send') || text.includes('发送') || + text.includes('提交') || text === '' || + className.includes('send') || className.includes('submit') || + className.includes('icon') || className.includes('button')) { + if (isVisible(btn)) { + return btn; + } + } + } + + console.error('[AI Panel] Kimi: No send button found!'); + return null; + } + + function setupResponseObserver() { + // Kimi response detection + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.addedNodes.length > 0) { + checkForNewMessages(); + } + } + }); + + // Start observing when DOM is ready + const startObserving = () => { + const chatContainer = findChatContainer(); + if (chatContainer) { + observer.observe(chatContainer, { + childList: true, + subtree: true + }); + } + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', startObserving); + } else { + startObserving(); + } + } + + function findChatContainer() { + // Kimi chat container selectors + const selectors = [ + 'div[class*="chat"]', + 'div[class*="message"]', + 'div[class*="conversation"]', + 'main', + '[role="main"]', + 'div[id*="chat"]', + 'div[id*="message"]' + ]; + + for (const selector of selectors) { + const container = document.querySelector(selector); + if (container) { + return container; + } + } + + return document.body; + } + + function checkForNewMessages() { + // Find assistant messages (Kimi's responses) + const messageSelectors = [ + 'div[class*="message"][class*="assistant"]', + 'div[class*="chat"] div[class*="assistant"]', + 'div[data-message-role="assistant"]', + 'div[class*="markdown"]', + 'div[class*="markdown-prose"]', + 'article', + '[data-testid*="assistant"]', + '[data-message-id]' + ]; + + for (const selector of messageSelectors) { + const messages = document.querySelectorAll(selector); + if (messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + const text = extractText(lastMessage); + + if (text && text.length > 0) { + latestResponse = text; + } + } + } + } + + function extractText(element) { + // Extract text content, handling potential nested structures + let text = element.textContent || element.innerText || ''; + return text.trim(); + } + + function getLatestResponse() { + return latestResponse; + } + + function waitForStreamingComplete() { + // Wait for response to complete (Kimi streams responses) + let lastLength = 0; + let stableCount = 0; + + const checkInterval = setInterval(() => { + checkForNewMessages(); + + const currentLength = latestResponse.length; + + if (currentLength === lastLength) { + stableCount++; + // If content hasn't changed for 10 checks (2 seconds), consider complete + if (stableCount >= 10) { + clearInterval(checkInterval); + } + } else { + stableCount = 0; + lastLength = currentLength; + } + }, 200); + + // Timeout after 2 minutes + setTimeout(() => { + clearInterval(checkInterval); + }, 120000); + } + + function isVisible(element) { + if (!element) return false; + const style = window.getComputedStyle(element); + return style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0' && + element.offsetParent !== null; + } + + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + async function simulateTyping(element, text) { + // Focus the element + element.focus(); + await sleep(50); + + // Clear existing content + element.innerHTML = ''; + await sleep(50); + + // Type each character + for (let i = 0; i < text.length; i++) { + const char = text[i]; + + // Dispatch keydown event + element.dispatchEvent(new KeyboardEvent('keydown', { + key: char, + code: 'Key' + char.toUpperCase(), + keyCode: char.charCodeAt(0), + which: char.charCodeAt(0), + bubbles: true, + cancelable: true + })); + + // Insert character + document.execCommand('insertText', false, char); + + // Dispatch keyup event + element.dispatchEvent(new KeyboardEvent('keyup', { + key: char, + code: 'Key' + char.toUpperCase(), + keyCode: char.charCodeAt(0), + which: char.charCodeAt(0), + bubbles: true, + cancelable: true + })); + + // Small delay between characters + await sleep(10); + } + + // Dispatch input event after typing + element.dispatchEvent(new Event('input', { bubbles: true })); + element.dispatchEvent(new Event('change', { bubbles: true })); + } +})(); diff --git a/content/qwen.js b/content/qwen.js new file mode 100644 index 0000000..56dfd81 --- /dev/null +++ b/content/qwen.js @@ -0,0 +1,372 @@ +// AI Panel - Qwen (通义千问) Content Script + +(function() { + 'use strict'; + + const AI_TYPE = 'qwen'; + + // State tracking + let isSending = false; + let latestResponse = ''; + + // Check if extension context is still valid + function isContextValid() { + return chrome.runtime && chrome.runtime.id; + } + + // Safe message sender that checks context first + function safeSendMessage(message, callback) { + if (!isContextValid()) { + console.log('[AI Panel] Extension context invalidated, skipping message'); + return; + } + try { + chrome.runtime.sendMessage(message, callback); + } catch (e) { + console.log('[AI Panel] Failed to send message:', e.message); + } + } + + // Notify background that content script is ready + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + + // Listen for messages from background script + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'INJECT_MESSAGE') { + injectMessage(message.message) + .then(() => { + sendResponse({ success: true }); + }) + .catch(err => { + console.error('[AI Panel] Qwen injection error:', err); + sendResponse({ success: false, error: err.message }); + }); + return true; // Keep channel open for async response + } + + if (message.type === 'GET_LATEST_RESPONSE') { + const response = getLatestResponse(); + sendResponse({ content: response }); + return true; + } + + return false; + }); + + // Setup response observer for cross-reference feature + setupResponseObserver(); + + // Ensure content script is ready even if page loads dynamically + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + }); + } else { + safeSendMessage({ type: 'CONTENT_SCRIPT_READY', aiType: AI_TYPE }); + } + + async function injectMessage(text) { + // Prevent duplicate sending + if (isSending) { + return false; + } + isSending = true; + + try { + // Qwen uses textarea for input + const inputSelectors = [ + 'textarea[placeholder*="输入"]', + 'textarea[placeholder*="message"]', + 'textarea[placeholder*="Message"]', + 'textarea[placeholder*="问通义"]', + 'textarea[placeholder*="和千问"]', + 'textarea[placeholder*="向通义提问"]', + 'textarea[placeholder*="发给通义千问"]', + 'textarea[class*="input"]', + 'textarea[class*="textarea"]', + 'div[contenteditable="true"]', + 'textarea' + ]; + + let inputEl = null; + + for (const selector of inputSelectors) { + const el = document.querySelector(selector); + if (el && isVisible(el)) { + inputEl = el; + break; + } + } + + if (!inputEl) { + throw new Error('Could not find input field'); + } + + // Focus the input + inputEl.focus(); + + // Handle different input types + if (inputEl.tagName === 'TEXTAREA') { + // For textarea, use value setter + inputEl.value = text; + + // Trigger React/Vue change events + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + )?.set; + + if (nativeInputValueSetter) { + nativeInputValueSetter.call(inputEl, text); + } + + // Dispatch events to trigger UI updates + inputEl.dispatchEvent(new Event('input', { bubbles: true })); + inputEl.dispatchEvent(new Event('change', { bubbles: true })); + inputEl.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true })); + } else { + // Contenteditable div + inputEl.textContent = text; + inputEl.innerText = text; + inputEl.dispatchEvent(new Event('input', { bubbles: true })); + inputEl.dispatchEvent(new Event('keyup', { bubbles: true })); + } + + // Small delay to let the UI process + await sleep(300); + + // Find the send button + const sendButton = findSendButton(); + if (sendButton) { + // Try to click send button + await clickSendButton(sendButton); + } + + // Start capturing response + setTimeout(() => { + waitForStreamingComplete(); + }, 2000); + + return true; + } finally { + // Reset sending state after a delay + setTimeout(() => { + isSending = false; + }, 1000); + } + } + + async function clickSendButton(button) { + // Try multiple methods to click the button + try { + // Method 1: Standard click + button.click(); + await sleep(100); + + // Method 2: Mouse events + const mouseDown = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }); + const mouseUp = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }); + const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }); + + button.dispatchEvent(mouseDown); + await sleep(50); + button.dispatchEvent(mouseUp); + await sleep(50); + button.dispatchEvent(clickEvent); + } catch (err) { + console.error('[AI Panel] Qwen: Click error, may need manual click'); + // Highlight button if click fails + button.style.boxShadow = '0 0 10px 3px rgba(66, 153, 225, 0.6)'; + setTimeout(() => { + button.style.boxShadow = ''; + }, 3000); + } + } + + function findSendButton() { + // Qwen send button selectors + const selectors = [ + 'span[data-icon-type="qwpcicon-sendChat"]', // Qwen specific + 'div[class*="operateBtn"] span[data-icon-type="qwpcicon-sendChat"]', // With container + 'svg use[xlink:href="#qwpcicon-sendChat"]', // SVG use element + 'div[class*="operateBtn"]', // Container itself + 'button[aria-label="Send"]', + 'button[aria-label="send"]', + 'button[aria-label="发送"]', + 'button[type="submit"]', + 'button[class*="send"]', + 'button[class*="submit"]', + 'div[role="button"][class*="send"]', + 'button:has(svg)' + ]; + + for (const selector of selectors) { + const btn = document.querySelector(selector); + if (btn && isVisible(btn)) { + // If it's use or svg element, return its container + if (btn.tagName === 'USE' || btn.tagName === 'svg' || btn.tagName === 'SPAN') { + const container = btn.closest('.operateBtn-JsB9e2') || btn.closest('div[class*="operateBtn"]') || btn.parentElement; + if (container) { + return container; + } + } + return btn; + } + } + + // Fallback: try to find any button that looks like a send button + const buttons = Array.from(document.querySelectorAll('button, div[class*="operateBtn"], span[data-icon-type], svg, use')); + + for (const btn of buttons) { + const text = btn.textContent?.toLowerCase() || ''; + const className = btn.className?.toLowerCase() || ''; + const dataset = btn.dataset || {}; + + if (text.includes('send') || text.includes('发送') || + text.includes('提交') || text === '' || + className.includes('operatebtn') || className.includes('send') || + className.includes('submit') || dataset?.iconType?.includes('sendChat')) { + if (isVisible(btn)) { + // If it's SVG/USE/SPAN, return container + if (btn.tagName === 'USE' || btn.tagName === 'svg' || btn.tagName === 'SPAN') { + const container = btn.closest('div[class*="operateBtn"]') || btn.parentElement; + if (container) { + return container; + } + } + return btn; + } + } + } + + console.error('[AI Panel] Qwen: No send button found!'); + return null; + } + + function setupResponseObserver() { + // Qwen response detection + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.addedNodes.length > 0) { + checkForNewMessages(); + } + } + }); + + // Start observing when DOM is ready + const startObserving = () => { + const chatContainer = findChatContainer(); + if (chatContainer) { + observer.observe(chatContainer, { + childList: true, + subtree: true + }); + } + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', startObserving); + } else { + startObserving(); + } + } + + function findChatContainer() { + // Qwen chat container selectors + const selectors = [ + 'div[class*="chat"]', + 'div[class*="message"]', + 'div[class*="conversation"]', + 'main', + '[role="main"]', + 'div[id*="chat"]', + 'div[id*="message"]' + ]; + + for (const selector of selectors) { + const container = document.querySelector(selector); + if (container) { + return container; + } + } + + return document.body; + } + + function checkForNewMessages() { + // Find assistant messages (Qwen's responses) + const messageSelectors = [ + 'div[class*="message"][class*="assistant"]', + 'div[class*="chat"] div[class*="assistant"]', + 'div[data-message-role="assistant"]', + 'div[class*="markdown"]', + 'div[class*="markdown-prose"]', + 'article', + '[data-testid*="assistant"]', + '[data-message-id]' + ]; + + for (const selector of messageSelectors) { + const messages = document.querySelectorAll(selector); + if (messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + const text = extractText(lastMessage); + + if (text && text.length > 0) { + latestResponse = text; + } + } + } + } + + function extractText(element) { + // Extract text content, handling potential nested structures + let text = element.textContent || element.innerText || ''; + return text.trim(); + } + + function getLatestResponse() { + return latestResponse; + } + + function waitForStreamingComplete() { + // Wait for response to complete (Qwen streams responses) + let lastLength = 0; + let stableCount = 0; + + const checkInterval = setInterval(() => { + checkForNewMessages(); + + const currentLength = latestResponse.length; + + if (currentLength === lastLength) { + stableCount++; + // If content hasn't changed for 10 checks (2 seconds), consider complete + if (stableCount >= 10) { + clearInterval(checkInterval); + } + } else { + stableCount = 0; + lastLength = currentLength; + } + }, 200); + + // Timeout after 2 minutes + setTimeout(() => { + clearInterval(checkInterval); + }, 120000); + } + + function isVisible(element) { + if (!element) return false; + const style = window.getComputedStyle(element); + return style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0' && + element.offsetParent !== null; + } + + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } +})(); diff --git a/manifest.json b/manifest.json index 7fc8826..3c1c2c2 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, - "name": "AI 圆桌 - Multi-AI Roundtable", - "version": "0.1.5", - "description": "让多个 AI 助手围桌讨论,交叉评价,深度协作", + "name": "AI 圆桌 中国版 - Multi-AI Roundtable CN", + "version": "0.5.4", + "description": "让多个 AI 助手围桌讨论,交叉评价,深度协作 - 中国版支持DeepSeek、Kimi、ChatGLM、通义千问等国产大模型", "permissions": [ "sidePanel", @@ -16,7 +16,13 @@ "https://claude.ai/*", "https://chat.openai.com/*", "https://chatgpt.com/*", - "https://gemini.google.com/*" + "https://gemini.google.com/*", + "https://chat.deepseek.com/*", + "https://kimi.moonshot.cn/*", + "https://www.kimi.com/*", + "https://chatglm.cn/*", + "https://tongyi.aliyun.com/*", + "https://www.qianwen.com/*" ], "side_panel": { @@ -42,6 +48,26 @@ "matches": ["https://gemini.google.com/*"], "js": ["content/gemini.js"], "run_at": "document_idle" + }, + { + "matches": ["https://chat.deepseek.com/*"], + "js": ["content/deepseek.js"], + "run_at": "document_idle" + }, + { + "matches": ["https://kimi.moonshot.cn/*", "https://www.kimi.com/*"], + "js": ["content/kimi.js"], + "run_at": "document_idle" + }, + { + "matches": ["https://chatglm.cn/*"], + "js": ["content/chatglm.js"], + "run_at": "document_idle" + }, + { + "matches": ["https://tongyi.aliyun.com/*", "https://www.qianwen.com/*"], + "js": ["content/qwen.js"], + "run_at": "document_idle" } ], diff --git a/sidepanel/panel.css b/sidepanel/panel.css index b8212d3..87999d2 100644 --- a/sidepanel/panel.css +++ b/sidepanel/panel.css @@ -108,6 +108,23 @@ header .subtitle { color: #3b82f6; } +/* Chinese AI Models Brand Colors */ +.target-name.deepseek { + color: #4D6BFE; +} + +.target-name.kimi { + color: #1677FF; +} + +.target-name.chatglm { + color: #52C41A; +} + +.target-name.qwen { + color: #722ED1; +} + .status { font-size: 11px; padding: 2px 6px; @@ -229,6 +246,39 @@ header .subtitle { border-color: #3b82f6; } +/* Chinese AI Models Brand Colors */ +.mention-btn.deepseek { + color: #4D6BFE; +} +.mention-btn.deepseek:hover { + background: rgba(77, 107, 254, 0.15); + border-color: #4D6BFE; +} + +.mention-btn.kimi { + color: #1677FF; +} +.mention-btn.kimi:hover { + background: rgba(22, 119, 255, 0.15); + border-color: #1677FF; +} + +.mention-btn.chatglm { + color: #52C41A; +} +.mention-btn.chatglm:hover { + background: rgba(82, 196, 26, 0.15); + border-color: #52C41A; +} + +.mention-btn.qwen { + color: #722ED1; +} +.mention-btn.qwen:hover { + background: rgba(114, 46, 209, 0.15); + border-color: #722ED1; +} + #message-input { width: 100%; padding: 12px; @@ -806,6 +856,10 @@ header .subtitle { #summary-content .ai-name.claude { color: #d97706; } #summary-content .ai-name.chatgpt { color: #10b981; } #summary-content .ai-name.gemini { color: #3b82f6; } +#summary-content .ai-name.deepseek { color: #4D6BFE; } +#summary-content .ai-name.kimi { color: #1677FF; } +#summary-content .ai-name.chatglm { color: #52C41A; } +#summary-content .ai-name.qwen { color: #722ED1; } #new-discussion-btn { width: 100%; diff --git a/sidepanel/panel.html b/sidepanel/panel.html index 9c70106..d16c5ed 100644 --- a/sidepanel/panel.html +++ b/sidepanel/panel.html @@ -9,8 +9,8 @@
-

AI 圆桌

-

Multi-AI Roundtable

+

AI 圆桌 中国版

+

Multi-AI Roundtable 中国版 - 支持国产大模型

@@ -23,17 +23,37 @@

AI 圆桌

+ + + + @@ -55,6 +75,10 @@

AI 圆桌

@ + + + + @@ -92,11 +116,27 @@

开始讨论

+ + + +