From e70d6293caa8457dcd49ae71d76551859341026a Mon Sep 17 00:00:00 2001 From: zhangxiong <87368512@qq.com> Date: Thu, 22 Jan 2026 13:21:41 +0800 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20v0.3.0=20-=20=E7=AC=AC=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E7=A8=B3=E5=AE=9A=E7=89=88=E6=9C=AC=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9EDeepSeek=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 新增功能 - ✅ 完整集成DeepSeek平台(国产大模型) - ✅ 消息自动填入到DeepSeek输入框 - ✅ 响应自动捕获和同步 - ✅ 发送按钮高亮提示(需手动点击) - ✅ UI完整集成(侧边栏、@提及、讨论模式) ## 修改文件 - manifest.json: 版本0.3.0,添加DeepSeek权限 - content/deepseek.js: 新增DeepSeek适配脚本 - sidepanel/panel.html: 添加DeepSeek UI元素 - sidepanel/panel.js: 添加DeepSeek到AI类型列表 - background.js: 添加DeepSeek URL模式 - README.md: 更新文档,说明DeepSeek支持 ## 新增文档 - 版本日志.md: 记录v0.2.0到v0.3.0的完整开发历程 - 添加国产大模型指南.md: 技术实现指南 - 重新加载指南.md: 用户安装和调试指南 ## 注意事项 - DeepSeek采用手动点击模式(技术限制) - 其他平台保持完全自动化支持 - 清理了所有调试代码,优化了用户体验 Co-Authored-By: Claude Sonnet 4.5 --- README.md | 55 ++- background.js | 5 +- content/deepseek.js | 326 ++++++++++++++++++ manifest.json | 14 +- sidepanel/panel.html | 14 +- sidepanel/panel.js | 10 +- ...41\345\236\213\346\214\207\345\215\227.md" | 186 ++++++++++ ...10\346\234\254\346\227\245\345\277\227.md" | 233 +++++++++++++ ...40\350\275\275\346\214\207\345\215\227.md" | 188 ++++++++++ 9 files changed, 1005 insertions(+), 26 deletions(-) create mode 100644 content/deepseek.js create mode 100644 "\346\267\273\345\212\240\345\233\275\344\272\247\345\244\247\346\250\241\345\236\213\346\214\207\345\215\227.md" create mode 100644 "\347\211\210\346\234\254\346\227\245\345\277\227.md" create mode 100644 "\351\207\215\346\226\260\345\212\240\350\275\275\346\214\207\345\215\227.md" diff --git a/README.md b/README.md index bc9ad72..b915f53 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.3.0-brightgreen.svg)](https://github.com/firebear/ai-roundtable-cn) +[![Status: Stable](https://img.shields.io/badge/Status-Stable-success.svg)](#-稳定版本) -> 让多个 AI 助手围桌讨论,交叉评价,深度协作 +> 让多个 AI 助手围桌讨论,交叉评价,深度协作 - 支持国产大模型 -一个 Chrome 扩展,让你像"会议主持人"一样,同时操控多个 AI(Claude、ChatGPT、Gemini),实现真正的 AI 圆桌会议。 +一个 Chrome 扩展,让你像"会议主持人"一样,同时操控多个 AI(Claude、ChatGPT、Gemini、DeepSeek),实现真正的 AI 圆桌会议。 + +**中国版特色**:新增对DeepSeek等国产大模型的支持,让AI圆桌更适合中国用户。 @@ -86,6 +89,14 @@ I'm currently most satisfied with, and calibrated to, the **web chat experience* - **交叉引用** - 让 Claude 评价 ChatGPT 的回答,或反过来 - **讨论模式** - 两个 AI 就同一主题进行多轮深度讨论 - **无需 API** - 直接操作网页界面,使用你现有的 AI 订阅 +- **国产大模型支持** - 新增DeepSeek等国产AI平台,更适合中国用户 + +## 支持平台 + +- ✅ **Claude** - 完全自动化支持 +- ✅ **ChatGPT** - 完全自动化支持 +- ✅ **Gemini** - 完全自动化支持 +- ✅ **DeepSeek** - 自动填入+响应捕获,需手动点击发送 --- @@ -132,8 +143,9 @@ I'm currently most satisfied with, and calibrated to, the **web chat experience* - [Claude](https://claude.ai) - [ChatGPT](https://chatgpt.com) - [Gemini](https://gemini.google.com) + - [DeepSeek](https://chat.deepseek.com) 🆕 中国版新增 -2. 推荐使用 Chrome 的 Split Tab 功能,将 2 个 AI 页面并排显示 +2. 推荐使用 Chrome 的 Split Tab 功能,将 2-4 个 AI 页面并排显示 3. 点击扩展图标,打开侧边栏控制台 @@ -144,13 +156,14 @@ I'm currently most satisfied with, and calibrated to, the **web chat experience* ### 普通模式 **基本发送** -1. 勾选要发送的目标 AI(Claude / ChatGPT / Gemini) +1. 勾选要发送的目标 AI(Claude / ChatGPT / Gemini / DeepSeek) 2. 输入消息 3. 按 Enter 或点击「发送」按钮 + - **DeepSeek用户注意**:插件会自动填入消息并高亮发送按钮3秒,请手动点击发送按钮 **@ 提及语法** - 点击 @ 按钮快速插入 AI 名称 -- 或手动输入:`@Claude 你怎么看这个问题?` +- 或手动输入:`@Claude 你怎么看这个问题?`或`@DeepSeek 分析一下` **互评(推荐)** @@ -207,7 +220,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 +230,9 @@ ai-roundtable/ ├── content/ │ ├── claude.js # Claude 页面注入脚本 │ ├── chatgpt.js # ChatGPT 页面注入脚本 -│ └── gemini.js # Gemini 页面注入脚本 +│ ├── gemini.js # Gemini 页面注入脚本 +│ └── deepseek.js # DeepSeek 页面注入脚本 🆕 中国版新增 +├── 版本日志.md # 版本更新记录 🆕 └── icons/ # 扩展图标 ``` @@ -244,6 +259,9 @@ ai-roundtable/ ### Q: ChatGPT 回复很长时会超时吗? **A:** 不会。系统支持最长 10 分钟的回复捕获。 +### Q: DeepSeek为什么需要手动点击发送? +**A:** DeepSeek的发送按钮采用了特殊的事件处理机制,当前版本无法通过JavaScript自动触发。但响应捕获功能完全正常,只需手动点击一次发送按钮即可。我们会在未来版本中继续优化。 + --- ## 已知限制 @@ -251,6 +269,7 @@ ai-roundtable/ - 依赖各 AI 平台的 DOM 结构,平台更新可能导致功能失效 - 讨论模式固定 2 个参与者 - 不支持 Claude Artifacts、ChatGPT Canvas 等特殊功能 +- **DeepSeek需要手动点击发送按钮**(自动填入和响应捕获功能正常) --- @@ -274,17 +293,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) + +**中国版 (Fork)**: [firebear](https://github.com/firebear) -### Learn More +- GitHub: [@firebear](https://github.com/firebear) +- Repository: [AI Roundtable CN](https://github.com/firebear/ai-roundtable-cn) -- [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 +**中国版更新**: +- 新增DeepSeek等国产大模型支持 +- 完善中文文档和使用指南 +- 优化中国用户体验 --- -© AXTONLIU™ & AI 精英学院™ 版权所有 +原项目 © AXTONLIU™ & AI 精英学院™ 版权所有 + +中国版修改部分遵循相同MIT许可证 diff --git a/background.js b/background.js index dc50c7e..cddfc49 100644 --- a/background.js +++ b/background.js @@ -4,13 +4,14 @@ 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'] }; // 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 }; } async function setStoredResponse(aiType, content) { diff --git a/content/deepseek.js b/content/deepseek.js new file mode 100644 index 0000000..c69155f --- /dev/null +++ b/content/deepseek.js @@ -0,0 +1,326 @@ +// 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); + + // Find the send button + const sendButton = findSendButton(); + if (sendButton) { + // Highlight the send 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'; + + // Remove highlight after 3 seconds + setTimeout(() => { + sendButton.style.boxShadow = ''; + }, 3000); + } + + // Start capturing response (user will manually click send) + 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 = [ + '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')) { + if (isVisible(btn)) { + return btn; + } + } + } + + return null; + } + + 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/manifest.json b/manifest.json index 7fc8826..d557612 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.3.0", + "description": "让多个 AI 助手围桌讨论,交叉评价,深度协作 - 中国版支持DeepSeek等国产大模型", "permissions": [ "sidePanel", @@ -16,7 +16,8 @@ "https://claude.ai/*", "https://chat.openai.com/*", "https://chatgpt.com/*", - "https://gemini.google.com/*" + "https://gemini.google.com/*", + "https://chat.deepseek.com/*" ], "side_panel": { @@ -42,6 +43,11 @@ "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" } ], diff --git a/sidepanel/panel.html b/sidepanel/panel.html index 9c70106..9130915 100644 --- a/sidepanel/panel.html +++ b/sidepanel/panel.html @@ -9,8 +9,8 @@
-

AI 圆桌

-

Multi-AI Roundtable

+

AI 圆桌 中国版

+

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

@@ -37,6 +37,11 @@

AI 圆桌

Gemini - +
@@ -58,6 +63,7 @@

AI 圆桌

+