// ==UserScript== // @name AI 对话助手(一键同步多模型) // @name:zh-CN AI 对话助手(一键同步多模型) // @name:en AI Chat Assistant (One-click Sync Multi-Model) // @namespace https://github.com/YHangbin // @version 1.0 // @description 拒绝复制粘贴!一键将你的问题分发给 ChatGPT、Claude、Gemini、豆包、Kimi 等所有 AI 模型。在任意 AI 网站提问,脚本会自动将问题同步到其他已打开的 AI 标签页。助你快速横向对比模型效果,效率提升 10 倍。 // @description:en Do not copy and paste! Sync your questions to ChatGPT, Claude, Gemini, Doubao, Kimi and other AI models with one click. // @author Gemini 2.5 Pro & User // @match https://doubao.com/chat/* // @match https://www.doubao.com/chat/* // @match https://chat.qwen.ai/* // @match https://tongyi.com/* // @match https://www.tongyi.com/* // @match https://aistudio.google.com/* // @match https://gemini.google.com/* // @match https://chatgpt.com/* // @match https://yuanbao.tencent.com/* // @match https://chat.deepseek.com/* // @match https://kimi.com/* // @match https://www.kimi.com/* // @match https://claude.ai/* // @match https://grok.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_addStyle // @grant unsafeWindow // @run-at document-start // @license MIT // @downloadURL none // ==/UserScript== /* * ================================================================================================= * --- 功能简介与使用说明 --- * * 【AI 对话助手(一键同步多模型)】 * * 核心目标:拒绝复制粘贴,实现“一处提问,多处同步”。 * * 核心亮点: * 1. 光速同步: 在一个页面输入,所有模型即刻响应。 * 2. 双向互通: 不分“主次”,任何一个聊天窗口都可以作为控制台。 * 3. 原生体验: 非弹窗式设计,使用网站原生的输入框,保留所有富文本功能。 * 4. 广泛支持: 适配 ChatGPT, Claude, Gemini, 豆包, Kimi, 通义千问, DeepSeek, Grok 等。 * * 简易使用说明: * 1. 打开面板: 点击页面右下角的悬浮按钮。 * 2. 选择目标: * - 灰色 (点击启动): 标签页未打开,点击自动打开。 * - 蓝色边框 (待发送): 标签页已打开,点击即可选中。 * - 蓝色填充 (已选中): 问题将同步发送给这些模型。 * 3. 发送问题: 在当前输入框正常提问,脚本自动分发。 * 4. 管理模型: 点击齿轮图标,自定义显示哪些常用模型。 * ================================================================================================= */ (function () { 'use strict'; /** * @class AITabSync * @description Core application object for the AI Tab Sync userscript. * Encapsulates all state, configuration, and logic. */ const AITabSync = { // =================================================================================== // --- 1. State Management --- // =================================================================================== state: { thisSite: null, visibleTargets: [], selectedTargets: new Set(), isLoggingEnabled: false, isSubmitting: false, isProcessingTask: false, menuCommandId: null, tooltipTimeoutId: null, }, // =================================================================================== // --- 2. Configuration --- // =================================================================================== config: { SCRIPT_VERSION: '1.0', KEYS: { SHARED_QUERY: 'multi_sync_query_v1.0', ACTIVE_TABS: 'multi_sync_active_tabs_v1.0', LOGGING_ENABLED: 'multi_sync_logging_v1.0', VISIBLE_TARGETS: 'multi_sync_visible_targets_v1.0', }, TIMINGS: { HEARTBEAT_INTERVAL: 5000, STALE_THRESHOLD: 15000, CLEANUP_INTERVAL: 10000, SUBMIT_TIMEOUT: 20000, // Increased timeout for complex SPAs HUMAN_LIKE_DELAY: 500, FRESHNESS_THRESHOLD: 5000, TOOLTIP_DELAY: 300, }, DISPLAY_ORDER: ['AI_STUDIO', 'GEMINI', 'TONGYI', 'QWEN', 'YUANBAO', 'CHATGPT', 'CLAUDE', 'DOUBAO', 'DEEPSEEK', 'KIMI', 'GROK'], SITES: { GROK: { id: 'GROK', name: 'Grok', host: 'grok.com', url: 'https://grok.com/', apiPaths: ['/rest/app-chat/conversations/'], inputSelectors: ['div.tiptap.ProseMirror'], queryExtractor: (body) => { try { return JSON.parse(body)?.message || ''; } catch (e) { return ''; } }, }, CLAUDE: { id: 'CLAUDE', name: 'Claude', host: 'claude.ai', url: 'https://claude.ai/new', apiPaths: ['/api/organizations/', '/completion'], inputSelectors: ['div[contenteditable="true"][role="textbox"]'], queryExtractor: (body) => { try { return JSON.parse(body)?.prompt || ''; } catch (e) { return ''; } }, }, KIMI: { id: 'KIMI', name: 'Kimi', host: 'kimi.com', url: 'https://www.kimi.com/', apiPaths: ['/apiv2/kimi.gateway.chat.v1.ChatService/Chat'], inputSelectors: ['[data-lexical-editor="true"]'], queryExtractor: (body) => { try { const firstBraceIndex = body.indexOf('{'); const lastBraceIndex = body.lastIndexOf('}'); if (firstBraceIndex === -1 || lastBraceIndex < firstBraceIndex) return ''; const jsonString = body.substring(firstBraceIndex, lastBraceIndex + 1); return JSON.parse(jsonString)?.message?.blocks?.[0]?.text?.content || ''; } catch (e) { return ''; } }, }, GEMINI: { id: 'GEMINI', name: 'Gemini', host: 'gemini.google.com', url: 'https://gemini.google.com/app', apiPaths: ['/StreamGenerate'], inputSelectors: ['div.ql-editor[contenteditable="true"]'], queryExtractor: (body) => { try { const params = new URLSearchParams(body); const f_req = params.get('f.req'); if (!f_req) return ''; const outerArray = JSON.parse(f_req); const innerJsonString = outerArray?.[1]; if (!innerJsonString) return ''; const innerArray = JSON.parse(innerJsonString); const query = innerArray?.[0]?.[0]; return typeof query === 'string' ? query : ''; } catch (e) { return ''; } }, }, YUANBAO: { id: 'YUANBAO', name: '元宝', host: 'yuanbao.tencent.com', url: 'https://yuanbao.tencent.com/', apiPaths: ['/api/chat/'], inputSelectors: ['.ql-editor[contenteditable="true"]'], queryExtractor: (body) => { try { return JSON.parse(body)?.prompt || ''; } catch (e) { return ''; } }, }, DEEPSEEK: { id: 'DEEPSEEK', name: 'DeepSeek', host: 'chat.deepseek.com', url: 'https://chat.deepseek.com/', apiPaths: ['/api/v0/chat/completion'], inputSelectors: ['textarea[placeholder="给 DeepSeek 发送消息 "]'], queryExtractor: (body) => { try { return JSON.parse(body)?.prompt || ''; } catch (e) { return ''; } }, }, DOUBAO: { id: 'DOUBAO', name: '豆包', host: 'doubao.com', url: 'https://www.doubao.com/chat/', apiPaths: ['/samantha/chat/completion'], inputSelectors: ['textarea[data-testid="chat_input_input"]'], queryExtractor: (body) => { try { const outerJson = JSON.parse(body); const innerJsonString = outerJson?.messages?.[0]?.content; if (!innerJsonString) return ''; const innerJson = JSON.parse(innerJsonString); return innerJson?.text || ''; } catch (e) { return ''; } }, }, QWEN: { id: 'QWEN', name: 'Qwen', host: 'chat.qwen.ai', url: 'https://chat.qwen.ai/', apiPaths: ['/api/v2/chat/completions'], inputSelectors: ['textarea#chat-input', 'textarea[data-testid="yuntu-textarea"]'], queryExtractor: (body) => { try { return JSON.parse(body)?.messages?.slice(-1)?.[0]?.content || ''; } catch (e) { return ''; } }, }, TONGYI: { id: 'TONGYI', name: '通义', host: 'tongyi.com', url: 'https://www.tongyi.com/', apiPaths: ['/dialog/conversation'], inputSelectors: ['div[class*="textareaWrap"] textarea', 'textarea[class*="ant-input"]'], queryExtractor: (body) => { try { return JSON.parse(body)?.contents?.[0]?.content || ''; } catch (e) { return ''; } }, }, AI_STUDIO: { id: 'AI_STUDIO', name: 'AI Studio', host: 'aistudio.google.com', url: 'https://aistudio.google.com/prompts/new_chat', apiPaths: ['/GenerateContent'], inputSelectors: ['ms-autosize-textarea textarea'], queryExtractor: (body) => { try { const json = JSON.parse(body); const messages = json?.[1]; if (Array.isArray(messages)) { for (let i = messages.length - 1; i >= 0; i--) { const msgBlock = messages[i]; if (Array.isArray(msgBlock) && msgBlock[1] === 'user') { return msgBlock[0]?.[0]?.[1] || ''; } } } return ''; } catch (e) { return ''; } }, }, CHATGPT: { id: 'CHATGPT', name: 'ChatGPT', host: 'chatgpt.com', url: 'https://chatgpt.com/', apiPaths: ['/backend-api/conversation', '/backend-api/f/conversation'], inputSelectors: ['#prompt-textarea'], queryExtractor: (body) => { try { const json = JSON.parse(body); const lastMessage = json?.messages?.slice(-1)?.[0]; return lastMessage?.content?.parts?.[0] || ''; } catch (e) { return ''; } }, }, }, }, // =================================================================================== // --- 3. Cached Elements --- // =================================================================================== elements: { container: null, fab: null, chipsContainer: null, settingsModal: null, tooltip: null, }, // =================================================================================== // --- 4. Utility Methods --- // =================================================================================== utils: { log(message, ...optionalParams) { if (!AITabSync.state.isLoggingEnabled || typeof console === 'undefined') return; console.log(`%c[AI Sync v${AITabSync.config.SCRIPT_VERSION}] ${message}`, 'color: #1976D2; font-weight: bold;', ...optionalParams); }, waitFor(conditionFn, timeout, description) { return new Promise((resolve, reject) => { let result = conditionFn(); if (result) return resolve(result); let timeoutId = null; const observer = new MutationObserver(() => { result = conditionFn(); if (result) { if (timeoutId) clearTimeout(timeoutId); observer.disconnect(); resolve(result); } }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, }); timeoutId = setTimeout(() => { observer.disconnect(); const lastResult = conditionFn(); lastResult ? resolve(lastResult) : reject(new Error(`waitFor timed out after ${timeout}ms for: ${description}`)); }, timeout); }); }, deepQuerySelector(selector, root = document) { try { const el = root.querySelector(selector); if (el) return el; } catch (e) { /* ignore */ } for (const host of root.querySelectorAll('*')) { if (host.shadowRoot) { const found = AITabSync.utils.deepQuerySelector(selector, host.shadowRoot); if (found) return found; } } return null; }, getCurrentSiteInfo() { const { SITES } = AITabSync.config; const currentHost = window.location.hostname; if (currentHost.includes('chatgpt.com')) return SITES.CHATGPT; for (const siteKey in SITES) { if (Object.prototype.hasOwnProperty.call(SITES, siteKey) && currentHost.includes(SITES[siteKey].host)) { return SITES[siteKey]; } } return null; }, simulateInput(element, value) { element.focus(); const siteId = AITabSync.state.thisSite?.id; // Handle complex rich-text editors with specific event-based methods first. if (siteId === 'GROK') { // Grok (Tiptap/ProseMirror) responds well to simulated paste events. const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', value); const pasteEvent = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true, }); element.dispatchEvent(pasteEvent); } else if (siteId === 'KIMI') { // Kimi (Lexical) responds to 'beforeinput' events. const beforeInputEvent = new InputEvent('beforeinput', { bubbles: true, cancelable: true, composed: true, inputType: 'insertText', data: value, }); element.dispatchEvent(beforeInputEvent); } else if (element.isContentEditable || element.contentEditable === 'true') { // Generic handler for other contentEditable elements (like Gemini, Claude). if (siteId === 'CLAUDE') { element.innerHTML = `

${value}

`; // Claude expects a paragraph. } else { element.textContent = value; } element.dispatchEvent(new Event('input', { bubbles: true, composed: true })); } else if (element.tagName === 'TEXTAREA') { // Standard handler for