diff --git a/README.md b/README.md index 2c48533..32dd0c1 100644 --- a/README.md +++ b/README.md @@ -86,12 +86,14 @@ Interacts with Gemini, AI Studio, ChatGPT, and Claude UIs. Injects chat, capture - [`AIStudioProvider`](extension/providers/aistudio.js) - [`ChatGptProvider`](extension/providers/chatgpt.js) - [`ClaudeProvider`](extension/providers/claude.js) + - [`KimiK2Provider`](extension/providers/kimi_k2.js) **Supported Chat Interfaces:** - Gemini (`gemini.google.com`) - AI Studio (`aistudio.google.com`) - ChatGPT (`chatgpt.com`) - Claude (`claude.ai`) +- Kimi K2 (`k2.kimi.ai`) ChatGPT is a trademark of OpenAI. Gemini and AI Studio are trademarks of Google. Claude is a trademark of Anthropic. This project is not affiliated with, endorsed by, or sponsored by OpenAI, Google, or Anthropic. diff --git a/docs/provider-kimi-k2.md b/docs/provider-kimi-k2.md new file mode 100644 index 0000000..c154572 --- /dev/null +++ b/docs/provider-kimi-k2.md @@ -0,0 +1,34 @@ +# Kimi K2 Provider Architecture + +This document describes the `KimiK2Provider` used by the extension to automate the Kimi K2 chat interface. + +--- + +## 🧩 Overview + +`KimiK2Provider` is modeled after the existing AI Studio and ChatGPT providers. It sends messages and captures responses from `k2.kimi.ai` using DOM based monitoring by default with an optional debugger fallback. + +--- + +## ⚙️ Configurable Options + +```js +this.captureMethod = 'dom'; // or 'debugger' +this.debuggerUrlPattern = '*k2.kimi.ai/api/chat*'; +this.includeThinkingInMessage = false; +``` + +--- + +## 📌 DOM Selectors + +- Input field: `textarea, div[contenteditable="true"]` +- Send button: `button[type="submit"], button.send-btn` +- Response blocks: `.message.ai, .chat-message.ai` +- Typing indicator: `.typing, .loading` + +--- + +## ✅ Summary + +The provider offers a lightweight integration with Kimi K2 using the same structure as the other providers. It can capture responses via DOM observation or via Chrome debugger when configured. diff --git a/extension/background.js b/extension/background.js index 90a179a..0f187f6 100644 --- a/extension/background.js +++ b/extension/background.js @@ -36,7 +36,7 @@ let lastSuccessfullyProcessedMessageText = null; // Text of the last message suc const pendingRequestDetails = new Map(); // Stores { text: string } for active requests, keyed by requestId // Supported domains for chat interfaces -const supportedDomains = ['gemini.google.com', 'aistudio.google.com', 'chatgpt.com', 'claude.ai']; +const supportedDomains = ['gemini.google.com', 'aistudio.google.com', 'chatgpt.com', 'claude.ai', 'k2.kimi.ai']; // ===== DEBUGGER RELATED GLOBALS ===== const BG_LOG_PREFIX = '[BG DEBUGGER]'; diff --git a/extension/manifest.json b/extension/manifest.json index 9d2d1da..8d79ad6 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -17,6 +17,7 @@ "*://*.chatgpt.com/*", "*://*.aistudio.com/*", "*://*.claude.ai/*", + "*://*.k2.kimi.ai/*", "ws://localhost:*/" ], "background": { @@ -58,6 +59,15 @@ "content.js" ], "run_at": "document_idle" + }, + { + "matches": ["*://k2.kimi.ai/*"], + "js": [ + "providers/provider-utils.js", + "providers/kimi_k2.js", + "content.js" + ], + "run_at": "document_idle" } ], "action": { diff --git a/extension/providers/index.js b/extension/providers/index.js index 8716632..67a3be6 100644 --- a/extension/providers/index.js +++ b/extension/providers/index.js @@ -22,7 +22,8 @@ const providers = { 'gemini': window.geminiProvider, 'aistudio': window.aiStudioProvider, 'chatgpt': window.chatgptProvider, - 'claude': window.claudeProvider + 'claude': window.claudeProvider, + 'kimi_k2': window.kimiK2Provider }; // Get a provider by ID @@ -40,6 +41,8 @@ function detectProvider(url) { return providers.chatgpt; } else if (url.includes('claude.ai')) { return providers.claude; + } else if (url.includes('k2.kimi.ai')) { + return providers.kimi_k2; } // Default to aistudio if we can't detect diff --git a/extension/providers/kimi_k2.js b/extension/providers/kimi_k2.js new file mode 100644 index 0000000..88b1a86 --- /dev/null +++ b/extension/providers/kimi_k2.js @@ -0,0 +1,129 @@ +/* + * Chat Relay: Relay for AI Chat Interfaces + * Copyright (C) 2025 Jamison Moore + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +// AI Chat Relay - Kimi K2 Provider + +class KimiK2Provider { + constructor() { + this.name = 'KimiK2Provider'; + this.supportedDomains = ['k2.kimi.ai']; + + // --- START OF CONFIGURABLE PROPERTIES --- + this.captureMethod = 'dom'; // DOM capture by default + this.debuggerUrlPattern = '*k2.kimi.ai/api/chat*'; // Placeholder + this.includeThinkingInMessage = false; + // --- END OF CONFIGURABLE PROPERTIES --- + + this.inputSelector = 'textarea, div[contenteditable="true"]'; + this.sendButtonSelector = 'button[type="submit"], button.send-btn'; + this.responseSelector = '.message.ai, .chat-message.ai'; + this.thinkingIndicatorSelector = '.typing, .loading'; + + this.lastSentMessage = ''; + this.pendingResponseCallbacks = new Map(); + this.requestAccumulators = new Map(); + } + + async sendChatMessage(text) { + console.log(`[${this.name}] sendChatMessage called:`, text); + const inputElement = document.querySelector(this.inputSelector); + const sendButton = document.querySelector(this.sendButtonSelector); + if (!inputElement || !sendButton) { + console.error(`[${this.name}] Missing input (${this.inputSelector}) or send button (${this.sendButtonSelector})`); + return false; + } + + this.lastSentMessage = text; + + if (inputElement.tagName.toLowerCase() === 'div' && inputElement.contentEditable === 'true') { + inputElement.focus(); + inputElement.innerHTML = ''; + inputElement.textContent = text; + inputElement.dispatchEvent(new Event('input', { bubbles: true })); + } else { + inputElement.value = text; + inputElement.dispatchEvent(new Event('input', { bubbles: true })); + inputElement.focus(); + } + await new Promise(r => setTimeout(r, 300)); + + if (!sendButton.disabled && sendButton.getAttribute('aria-disabled') !== 'true') { + sendButton.click(); + return true; + } + + console.warn(`[${this.name}] Send button disabled.`); + return false; + } + + captureResponse(element) { + if (!element) return { found: false, text: '' }; + let text = element.textContent.trim(); + if (!text || text === this.lastSentMessage) return { found: false, text: '' }; + return { found: true, text }; + } + + initiateResponseCapture(requestId, callback) { + this.pendingResponseCallbacks.set(requestId, callback); + if (this.captureMethod === 'dom') { + console.log(`[${this.name}] DOM capture active for ${requestId}`); + } + } + + handleDebuggerData(requestId, rawData, isFinal) { + const cb = this.pendingResponseCallbacks.get(requestId); + if (!cb) return; + + const parsed = this.parseDebuggerResponse(rawData); + if (parsed.text || isFinal) { + cb(requestId, parsed.text, isFinal); + } + if (isFinal) { + this.pendingResponseCallbacks.delete(requestId); + } + } + + parseDebuggerResponse(raw) { + if (!raw) return { text: '', isFinalResponse: false }; + if (raw.includes('[DONE]')) { + const clean = raw.replace('[DONE]', '').trim(); + return { text: clean, isFinalResponse: true }; + } + return { text: raw.trim(), isFinalResponse: false }; + } + + getStreamingApiPatterns() { + if (this.captureMethod === 'debugger' && this.debuggerUrlPattern) { + return [{ urlPattern: this.debuggerUrlPattern, requestStage: 'Response' }]; + } + return []; + } + + shouldSkipResponseMonitoring() { + return this.captureMethod === 'debugger'; + } +} + +(function() { + if (window.providerUtils) { + const providerInstance = new KimiK2Provider(); + window.providerUtils.registerProvider(providerInstance.name, providerInstance.supportedDomains, providerInstance); + } else { + console.error('ProviderUtils not found. KimiK2Provider cannot be registered.'); + } +})(); +